Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the Android Debug Bridge integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import os
7 from typing import Any
8 
9 from androidtv import state_detection_rules_validator
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import (
13  ConfigEntry,
14  ConfigFlow,
15  ConfigFlowResult,
16  OptionsFlow,
17 )
18 from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT
19 from homeassistant.core import callback
20 from homeassistant.helpers import config_validation as cv
22  ObjectSelector,
23  SelectOptionDict,
24  SelectSelector,
25  SelectSelectorConfig,
26  SelectSelectorMode,
27 )
28 
29 from . import async_connect_androidtv, get_androidtv_mac
30 from .const import (
31  CONF_ADB_SERVER_IP,
32  CONF_ADB_SERVER_PORT,
33  CONF_ADBKEY,
34  CONF_APPS,
35  CONF_EXCLUDE_UNNAMED_APPS,
36  CONF_GET_SOURCES,
37  CONF_SCREENCAP_INTERVAL,
38  CONF_STATE_DETECTION_RULES,
39  CONF_TURN_OFF_COMMAND,
40  CONF_TURN_ON_COMMAND,
41  DEFAULT_ADB_SERVER_PORT,
42  DEFAULT_DEVICE_CLASS,
43  DEFAULT_EXCLUDE_UNNAMED_APPS,
44  DEFAULT_GET_SOURCES,
45  DEFAULT_PORT,
46  DEFAULT_SCREENCAP_INTERVAL,
47  DEVICE_CLASSES,
48  DOMAIN,
49  PROP_ETHMAC,
50  PROP_WIFIMAC,
51 )
52 
53 APPS_NEW_ID = "NewApp"
54 CONF_APP_DELETE = "app_delete"
55 CONF_APP_ID = "app_id"
56 CONF_APP_NAME = "app_name"
57 
58 RULES_NEW_ID = "NewRule"
59 CONF_RULE_DELETE = "rule_delete"
60 CONF_RULE_ID = "rule_id"
61 CONF_RULE_VALUES = "rule_values"
62 
63 RESULT_CONN_ERROR = "cannot_connect"
64 RESULT_UNKNOWN = "unknown"
65 
66 _LOGGER = logging.getLogger(__name__)
67 
68 
69 def _is_file(value: str) -> bool:
70  """Validate that the value is an existing file."""
71  file_in = os.path.expanduser(value)
72  return os.path.isfile(file_in) and os.access(file_in, os.R_OK)
73 
74 
75 class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN):
76  """Handle a config flow."""
77 
78  VERSION = 1
79  MINOR_VERSION = 2
80 
81  @callback
83  self,
84  user_input: dict[str, Any] | None = None,
85  error: str | None = None,
86  ) -> ConfigFlowResult:
87  """Show the setup form to the user."""
88  host = user_input.get(CONF_HOST, "") if user_input else ""
89  data_schema = vol.Schema(
90  {
91  vol.Required(CONF_HOST, default=host): str,
92  vol.Required(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In(
93  DEVICE_CLASSES
94  ),
95  vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
96  },
97  )
98 
99  if self.show_advanced_optionsshow_advanced_options:
100  data_schema = data_schema.extend(
101  {
102  vol.Optional(CONF_ADBKEY): str,
103  vol.Optional(CONF_ADB_SERVER_IP): str,
104  vol.Required(
105  CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT
106  ): cv.port,
107  }
108  )
109 
110  return self.async_show_formasync_show_formasync_show_form(
111  step_id="user",
112  data_schema=data_schema,
113  errors={"base": error} if error else None,
114  )
115 
117  self, user_input: dict[str, Any]
118  ) -> tuple[str | None, str | None]:
119  """Attempt to connect the Android device."""
120 
121  try:
122  aftv, error_message = await async_connect_androidtv(self.hass, user_input)
123  except Exception:
124  _LOGGER.exception(
125  "Unknown error connecting with Android device at %s",
126  user_input[CONF_HOST],
127  )
128  return RESULT_UNKNOWN, None
129 
130  if not aftv:
131  _LOGGER.warning(error_message)
132  return RESULT_CONN_ERROR, None
133 
134  dev_prop = aftv.device_properties
135  _LOGGER.debug(
136  "Android device at %s: %s = %r, %s = %r",
137  user_input[CONF_HOST],
138  PROP_ETHMAC,
139  dev_prop.get(PROP_ETHMAC),
140  PROP_WIFIMAC,
141  dev_prop.get(PROP_WIFIMAC),
142  )
143  unique_id = get_androidtv_mac(dev_prop)
144  await aftv.adb_close()
145  return None, unique_id
146 
147  async def async_step_user(
148  self, user_input: dict[str, Any] | None = None
149  ) -> ConfigFlowResult:
150  """Handle a flow initiated by the user."""
151  error = None
152 
153  if user_input is not None:
154  host = user_input[CONF_HOST]
155  adb_key = user_input.get(CONF_ADBKEY)
156  if CONF_ADB_SERVER_IP in user_input:
157  if adb_key:
158  return self._show_setup_form_show_setup_form(user_input, "key_and_server")
159  else:
160  user_input.pop(CONF_ADB_SERVER_PORT, None)
161 
162  if adb_key:
163  if not await self.hass.async_add_executor_job(_is_file, adb_key):
164  return self._show_setup_form_show_setup_form(user_input, "adbkey_not_file")
165 
166  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
167  error, unique_id = await self._async_check_connection_async_check_connection(user_input)
168  if error is None:
169  if not unique_id:
170  return self.async_abortasync_abortasync_abort(reason="invalid_unique_id")
171 
172  await self.async_set_unique_idasync_set_unique_id(unique_id)
173  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
174 
175  return self.async_create_entryasync_create_entryasync_create_entry(
176  title=host,
177  data=user_input,
178  )
179 
180  return self._show_setup_form_show_setup_form(user_input, error)
181 
182  @staticmethod
183  @callback
184  def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
185  """Get the options flow for this handler."""
186  return OptionsFlowHandler(config_entry)
187 
188 
190  """Handle an option flow for Android Debug Bridge."""
191 
192  def __init__(self, config_entry: ConfigEntry) -> None:
193  """Initialize options flow."""
194  self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
195  self._state_det_rules: dict[str, Any] = dict(
196  config_entry.options.get(CONF_STATE_DETECTION_RULES, {})
197  )
198  self._conf_app_id_conf_app_id: str | None = None
199  self._conf_rule_id_conf_rule_id: str | None = None
200 
201  @callback
202  def _save_config(self, data: dict[str, Any]) -> ConfigFlowResult:
203  """Save the updated options."""
204  new_data = {
205  k: v
206  for k, v in data.items()
207  if k not in [CONF_APPS, CONF_STATE_DETECTION_RULES]
208  }
209  if self._apps:
210  new_data[CONF_APPS] = self._apps
211  if self._state_det_rules:
212  new_data[CONF_STATE_DETECTION_RULES] = self._state_det_rules
213 
214  return self.async_create_entryasync_create_entry(title="", data=new_data)
215 
216  async def async_step_init(
217  self, user_input: dict[str, Any] | None = None
218  ) -> ConfigFlowResult:
219  """Handle options flow."""
220  if user_input is not None:
221  if sel_app := user_input.get(CONF_APPS):
222  return await self.async_step_appsasync_step_apps(None, sel_app)
223  if sel_rule := user_input.get(CONF_STATE_DETECTION_RULES):
224  return await self.async_step_rulesasync_step_rules(None, sel_rule)
225  return self._save_config_save_config(user_input)
226 
227  return self._async_init_form_async_init_form()
228 
229  @callback
230  def _async_init_form(self) -> ConfigFlowResult:
231  """Return initial configuration form."""
232 
233  apps_list = {k: f"{v} ({k})" if v else k for k, v in self._apps.items()}
234  apps = [SelectOptionDict(value=APPS_NEW_ID, label="Add new")] + [
235  SelectOptionDict(value=k, label=v) for k, v in apps_list.items()
236  ]
237  rules = [RULES_NEW_ID, *self._state_det_rules]
238  options = self.config_entryconfig_entryconfig_entry.options
239 
240  data_schema = vol.Schema(
241  {
242  vol.Optional(CONF_APPS): SelectSelector(
243  SelectSelectorConfig(options=apps, mode=SelectSelectorMode.DROPDOWN)
244  ),
245  vol.Optional(
246  CONF_GET_SOURCES,
247  default=options.get(CONF_GET_SOURCES, DEFAULT_GET_SOURCES),
248  ): bool,
249  vol.Optional(
250  CONF_EXCLUDE_UNNAMED_APPS,
251  default=options.get(
252  CONF_EXCLUDE_UNNAMED_APPS, DEFAULT_EXCLUDE_UNNAMED_APPS
253  ),
254  ): bool,
255  vol.Required(
256  CONF_SCREENCAP_INTERVAL,
257  default=options.get(
258  CONF_SCREENCAP_INTERVAL, DEFAULT_SCREENCAP_INTERVAL
259  ),
260  ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=15)),
261  vol.Optional(
262  CONF_TURN_OFF_COMMAND,
263  description={
264  "suggested_value": options.get(CONF_TURN_OFF_COMMAND, "")
265  },
266  ): str,
267  vol.Optional(
268  CONF_TURN_ON_COMMAND,
269  description={
270  "suggested_value": options.get(CONF_TURN_ON_COMMAND, "")
271  },
272  ): str,
273  vol.Optional(CONF_STATE_DETECTION_RULES): SelectSelector(
275  options=rules, mode=SelectSelectorMode.DROPDOWN
276  )
277  ),
278  }
279  )
280 
281  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
282 
283  async def async_step_apps(
284  self, user_input: dict[str, Any] | None = None, app_id: str | None = None
285  ) -> ConfigFlowResult:
286  """Handle options flow for apps list."""
287  if app_id is not None:
288  self._conf_app_id_conf_app_id = app_id if app_id != APPS_NEW_ID else None
289  return self._async_apps_form_async_apps_form(app_id)
290 
291  if user_input is not None:
292  app_id = user_input.get(CONF_APP_ID, self._conf_app_id_conf_app_id)
293  if app_id:
294  if user_input.get(CONF_APP_DELETE, False):
295  self._apps.pop(app_id)
296  else:
297  self._apps[app_id] = user_input.get(CONF_APP_NAME, "")
298 
299  return await self.async_step_initasync_step_init()
300 
301  @callback
302  def _async_apps_form(self, app_id: str) -> ConfigFlowResult:
303  """Return configuration form for apps."""
304  app_schema = {
305  vol.Optional(
306  CONF_APP_NAME,
307  description={"suggested_value": self._apps.get(app_id, "")},
308  ): str,
309  }
310  if app_id == APPS_NEW_ID:
311  data_schema = vol.Schema({**app_schema, vol.Optional(CONF_APP_ID): str})
312  else:
313  data_schema = vol.Schema(
314  {**app_schema, vol.Optional(CONF_APP_DELETE, default=False): bool}
315  )
316 
317  return self.async_show_formasync_show_form(
318  step_id="apps",
319  data_schema=data_schema,
320  description_placeholders={
321  "app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "",
322  },
323  )
324 
325  async def async_step_rules(
326  self, user_input: dict[str, Any] | None = None, rule_id: str | None = None
327  ) -> ConfigFlowResult:
328  """Handle options flow for detection rules."""
329  if rule_id is not None:
330  self._conf_rule_id_conf_rule_id = rule_id if rule_id != RULES_NEW_ID else None
331  return self._async_rules_form_async_rules_form(rule_id)
332 
333  if user_input is not None:
334  rule_id = user_input.get(CONF_RULE_ID, self._conf_rule_id_conf_rule_id)
335  if rule_id:
336  if user_input.get(CONF_RULE_DELETE, False):
337  self._state_det_rules.pop(rule_id)
338  elif det_rule := user_input.get(CONF_RULE_VALUES):
339  state_det_rule = _validate_state_det_rules(det_rule)
340  if state_det_rule is None:
341  return self._async_rules_form_async_rules_form(
342  rule_id=self._conf_rule_id_conf_rule_id or RULES_NEW_ID,
343  default_id=rule_id,
344  errors={"base": "invalid_det_rules"},
345  )
346  self._state_det_rules[rule_id] = state_det_rule
347 
348  return await self.async_step_initasync_step_init()
349 
350  @callback
352  self, rule_id: str, default_id: str = "", errors: dict[str, str] | None = None
353  ) -> ConfigFlowResult:
354  """Return configuration form for detection rules."""
355  rule_schema = {
356  vol.Optional(
357  CONF_RULE_VALUES, default=self._state_det_rules.get(rule_id)
358  ): ObjectSelector()
359  }
360  if rule_id == RULES_NEW_ID:
361  data_schema = vol.Schema(
362  {vol.Optional(CONF_RULE_ID, default=default_id): str, **rule_schema}
363  )
364  else:
365  data_schema = vol.Schema(
366  {**rule_schema, vol.Optional(CONF_RULE_DELETE, default=False): bool}
367  )
368 
369  return self.async_show_formasync_show_form(
370  step_id="rules",
371  data_schema=data_schema,
372  description_placeholders={
373  "rule_id": f"`{rule_id}`" if rule_id != RULES_NEW_ID else "",
374  },
375  errors=errors,
376  )
377 
378 
379 def _validate_state_det_rules(state_det_rules: Any) -> list[Any] | None:
380  """Validate a string that contain state detection rules and return a dict."""
381  json_rules = state_det_rules
382  if not isinstance(json_rules, list):
383  json_rules = [json_rules]
384 
385  try:
386  state_detection_rules_validator(json_rules, ValueError)
387  except ValueError as exc:
388  _LOGGER.warning("Invalid state detection rules: %s", exc)
389  return None
390  return json_rules # type: ignore[no-any-return]
tuple[str|None, str|None] _async_check_connection(self, dict[str, Any] user_input)
Definition: config_flow.py:118
ConfigFlowResult _show_setup_form(self, dict[str, Any]|None user_input=None, str|None error=None)
Definition: config_flow.py:86
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:149
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:184
ConfigFlowResult _save_config(self, dict[str, Any] data)
Definition: config_flow.py:202
ConfigFlowResult _async_rules_form(self, str rule_id, str default_id="", dict[str, str]|None errors=None)
Definition: config_flow.py:353
ConfigFlowResult async_step_rules(self, dict[str, Any]|None user_input=None, str|None rule_id=None)
Definition: config_flow.py:327
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:218
ConfigFlowResult async_step_apps(self, dict[str, Any]|None user_input=None, str|None app_id=None)
Definition: config_flow.py:285
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None config_entry(self, ConfigEntry value)
bool show_advanced_options(self)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
list[Any]|None _validate_state_det_rules(Any state_det_rules)
Definition: config_flow.py:379
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
str|None get_androidtv_mac(dict[str, Any] dev_props)
Definition: __init__.py:85
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88