Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for functionality to interact with Android/Fire TV devices."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from dataclasses import dataclass
7 import logging
8 import os
9 from typing import Any
10 
11 from adb_shell.auth.keygen import keygen
12 from adb_shell.exceptions import (
13  AdbTimeoutError,
14  InvalidChecksumError,
15  InvalidCommandError,
16  InvalidResponseError,
17  TcpTimeoutException,
18 )
19 from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner
20 from androidtv.setup_async import (
21  AndroidTVAsync,
22  FireTVAsync,
23  setup as async_androidtv_setup,
24 )
25 
26 from homeassistant.config_entries import ConfigEntry
27 from homeassistant.const import (
28  CONF_DEVICE_CLASS,
29  CONF_HOST,
30  CONF_PORT,
31  EVENT_HOMEASSISTANT_STOP,
32  Platform,
33 )
34 from homeassistant.core import Event, HomeAssistant
35 from homeassistant.exceptions import ConfigEntryNotReady
36 from homeassistant.helpers.device_registry import format_mac
37 from homeassistant.helpers.dispatcher import async_dispatcher_send
38 from homeassistant.helpers.storage import STORAGE_DIR
39 
40 from .const import (
41  CONF_ADB_SERVER_IP,
42  CONF_ADB_SERVER_PORT,
43  CONF_ADBKEY,
44  CONF_SCREENCAP_INTERVAL,
45  CONF_STATE_DETECTION_RULES,
46  DEFAULT_ADB_SERVER_PORT,
47  DEVICE_ANDROIDTV,
48  DEVICE_FIRETV,
49  PROP_ETHMAC,
50  PROP_WIFIMAC,
51  SIGNAL_CONFIG_ENTITY,
52 )
53 
54 ADB_PYTHON_EXCEPTIONS: tuple = (
55  AdbTimeoutError,
56  BrokenPipeError,
57  ConnectionResetError,
58  ValueError,
59  InvalidChecksumError,
60  InvalidCommandError,
61  InvalidResponseError,
62  TcpTimeoutException,
63 )
64 ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
65 
66 PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
67 RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
68 
69 _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"}
70 
71 _LOGGER = logging.getLogger(__name__)
72 
73 
74 @dataclass
76  """Runtime data definition."""
77 
78  aftv: AndroidTVAsync | FireTVAsync
79  dev_opt: dict[str, Any]
80 
81 
82 AndroidTVConfigEntry = ConfigEntry[AndroidTVRuntimeData]
83 
84 
85 def get_androidtv_mac(dev_props: dict[str, Any]) -> str | None:
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):
89  mac = format_mac(if_mac)
90  if mac not in _INVALID_MACS:
91  return mac
92  return None
93 
94 
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")
101  )
102  if CONF_ADB_SERVER_IP not in config:
103  # Use "adb_shell" (Python ADB implementation)
104  if not os.path.isfile(adbkey):
105  # Generate ADB key files
106  keygen(adbkey)
107 
108  # Load the ADB key
109  signer = ADBPythonSync.load_adbkey(adbkey)
110  adb_log = f"using Python ADB implementation with adbkey='{adbkey}'"
111 
112  else:
113  # Use "pure-python-adb" (communicate with ADB server)
114  signer = None
115  adb_log = (
116  "using ADB server at"
117  f" {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}"
118  )
119 
120  return adbkey, signer, adb_log
121 
122 
124  hass: HomeAssistant,
125  config: Mapping[str, Any],
126  *,
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]}"
132 
133  adbkey, signer, adb_log = await hass.async_add_executor_job(
134  _setup_androidtv, hass, config
135  )
136 
137  aftv = await async_androidtv_setup(
138  config[CONF_HOST],
139  config[CONF_PORT],
140  adbkey,
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],
145  timeout,
146  signer,
147  )
148 
149  if not aftv.available:
150  # Determine the name that will be used for the device in the log
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"
155  else:
156  device_name = "Android / Fire TV device"
157 
158  error_message = f"Could not connect to {device_name} at {address} {adb_log}"
159  return None, error_message
160 
161  return aftv, None
162 
163 
164 async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
165  """Migrate old entry."""
166  _LOGGER.debug(
167  "Migrating configuration from version %s.%s", entry.version, entry.minor_version
168  )
169 
170  if entry.version == 1:
171  new_options = {**entry.options}
172 
173  # Migrate MinorVersion 1 -> MinorVersion 2: New option
174  if entry.minor_version < 2:
175  new_options = {**new_options, CONF_SCREENCAP_INTERVAL: 0}
176 
177  hass.config_entries.async_update_entry(
178  entry, options=new_options, minor_version=2, version=1
179  )
180 
181  _LOGGER.debug(
182  "Migration to configuration version %s.%s successful",
183  entry.version,
184  entry.minor_version,
185  )
186 
187  return True
188 
189 
190 async def async_setup_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
191  """Set up Android Debug Bridge platform."""
192 
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
196  else:
197  exceptions = ADB_TCP_EXCEPTIONS
198 
199  try:
200  aftv, error_message = await async_connect_androidtv(
201  hass, entry.data, state_detection_rules=state_det_rules
202  )
203  except exceptions as exc:
204  raise ConfigEntryNotReady(exc) from exc
205 
206  if not aftv:
207  raise ConfigEntryNotReady(error_message)
208 
209  async def async_close_connection(event: Event) -> None:
210  """Close Android Debug Bridge connection on HA Stop."""
211  await aftv.adb_close()
212 
213  entry.async_on_unload(
214  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection)
215  )
216  entry.async_on_unload(entry.add_update_listener(update_listener))
217 
218  entry.runtime_data = AndroidTVRuntimeData(aftv, entry.options.copy())
219 
220  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
221 
222  return True
223 
224 
225 async def async_unload_entry(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> bool:
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()
230 
231  return unload_ok
232 
233 
234 async def update_listener(hass: HomeAssistant, entry: AndroidTVConfigEntry) -> None:
235  """Update when config_entry options update."""
236  reload_opt = False
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:
242  reload_opt = True
243  break
244 
245  if reload_opt:
246  await hass.config_entries.async_reload(entry.entry_id)
247  return
248 
249  entry.runtime_data.dev_opt = entry.options.copy()
250  async_dispatcher_send(hass, f"{SIGNAL_CONFIG_ENTITY}_{entry.entry_id}")
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)
Definition: __init__.py:129
bool async_unload_entry(HomeAssistant hass, AndroidTVConfigEntry entry)
Definition: __init__.py:225
tuple[str, PythonRSASigner|None, str] _setup_androidtv(HomeAssistant hass, Mapping[str, Any] config)
Definition: __init__.py:97
bool async_setup_entry(HomeAssistant hass, AndroidTVConfigEntry entry)
Definition: __init__.py:190
bool async_migrate_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:164
str|None get_androidtv_mac(dict[str, Any] dev_props)
Definition: __init__.py:85
None update_listener(HomeAssistant hass, AndroidTVConfigEntry entry)
Definition: __init__.py:234
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193