1 """Support for functionality to interact with Android/Fire TV devices."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from dataclasses
import dataclass
11 from adb_shell.auth.keygen
import keygen
12 from adb_shell.exceptions
import (
19 from androidtv.adb_manager.adb_manager_sync
import ADBPythonSync, PythonRSASigner
20 from androidtv.setup_async
import (
23 setup
as async_androidtv_setup,
31 EVENT_HOMEASSISTANT_STOP,
44 CONF_SCREENCAP_INTERVAL,
45 CONF_STATE_DETECTION_RULES,
46 DEFAULT_ADB_SERVER_PORT,
54 ADB_PYTHON_EXCEPTIONS: tuple = (
64 ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
66 PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
67 RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
69 _INVALID_MACS = {
"ff:ff:ff:ff:ff:ff"}
71 _LOGGER = logging.getLogger(__name__)
76 """Runtime data definition."""
78 aftv: AndroidTVAsync | FireTVAsync
79 dev_opt: dict[str, Any]
82 AndroidTVConfigEntry = ConfigEntry[AndroidTVRuntimeData]
86 """Return formatted mac from device properties."""
87 for prop_mac
in (PROP_ETHMAC, PROP_WIFIMAC):
88 if if_mac := dev_props.get(prop_mac):
90 if mac
not in _INVALID_MACS:
96 hass: HomeAssistant, config: Mapping[str, Any]
97 ) -> tuple[str, PythonRSASigner |
None, str]:
98 """Generate an ADB key (if needed) and load it."""
99 adbkey: str = config.get(
100 CONF_ADBKEY, hass.config.path(STORAGE_DIR,
"androidtv_adbkey")
102 if CONF_ADB_SERVER_IP
not in config:
104 if not os.path.isfile(adbkey):
109 signer = ADBPythonSync.load_adbkey(adbkey)
110 adb_log = f
"using Python ADB implementation with adbkey='{adbkey}'"
116 "using ADB server at"
117 f
" {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
120 return adbkey, signer, adb_log
125 config: Mapping[str, Any],
127 state_detection_rules: dict[str, Any] |
None =
None,
128 timeout: float = 30.0,
129 ) -> tuple[AndroidTVAsync | FireTVAsync |
None, str |
None]:
130 """Connect to Android device."""
131 address = f
"{config[CONF_HOST]}:{config[CONF_PORT]}"
133 adbkey, signer, adb_log = await hass.async_add_executor_job(
134 _setup_androidtv, hass, config
137 aftv = await async_androidtv_setup(
141 config.get(CONF_ADB_SERVER_IP),
142 config.get(CONF_ADB_SERVER_PORT, DEFAULT_ADB_SERVER_PORT),
143 state_detection_rules,
144 config[CONF_DEVICE_CLASS],
149 if not aftv.available:
151 if config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV:
152 device_name =
"Android device"
153 elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV:
154 device_name =
"Fire TV device"
156 device_name =
"Android / Fire TV device"
158 error_message = f
"Could not connect to {device_name} at {address} {adb_log}"
159 return None, error_message
165 """Migrate old entry."""
167 "Migrating configuration from version %s.%s", entry.version, entry.minor_version
170 if entry.version == 1:
171 new_options = {**entry.options}
174 if entry.minor_version < 2:
175 new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
177 hass.config_entries.async_update_entry(
178 entry, options=new_options, minor_version=2, version=1
182 "Migration to configuration version %s.%s successful",
191 """Set up Android Debug Bridge platform."""
193 state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES)
194 if CONF_ADB_SERVER_IP
not in entry.data:
195 exceptions = ADB_PYTHON_EXCEPTIONS
197 exceptions = ADB_TCP_EXCEPTIONS
201 hass, entry.data, state_detection_rules=state_det_rules
203 except exceptions
as exc:
209 async
def async_close_connection(event: Event) ->
None:
210 """Close Android Debug Bridge connection on HA Stop."""
211 await aftv.adb_close()
213 entry.async_on_unload(
214 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection)
216 entry.async_on_unload(entry.add_update_listener(update_listener))
220 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
226 """Unload a config entry."""
227 if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
228 aftv = entry.runtime_data.aftv
229 await aftv.adb_close()
235 """Update when config_entry options update."""
237 old_options = entry.runtime_data.dev_opt
238 for opt_key, opt_val
in entry.options.items():
239 if opt_key
in RELOAD_OPTIONS:
240 old_val = old_options.get(opt_key)
241 if old_val
is None or old_val != opt_val:
246 await hass.config_entries.async_reload(entry.entry_id)
249 entry.runtime_data.dev_opt = entry.options.copy()
tuple[AndroidTVAsync|FireTVAsync|None, str|None] async_connect_androidtv(HomeAssistant hass, Mapping[str, Any] config, *dict[str, Any]|None state_detection_rules=None, float timeout=30.0)
bool async_unload_entry(HomeAssistant hass, AndroidTVConfigEntry entry)
tuple[str, PythonRSASigner|None, str] _setup_androidtv(HomeAssistant hass, Mapping[str, Any] config)
bool async_setup_entry(HomeAssistant hass, AndroidTVConfigEntry entry)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry entry)
str|None get_androidtv_mac(dict[str, Any] dev_props)
None update_listener(HomeAssistant hass, AndroidTVConfigEntry entry)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)