Home Assistant Unofficial Reference 2024.12.1
firmware_config_flow.py
Go to the documentation of this file.
1 """Config flow for the Home Assistant SkyConnect integration."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 import asyncio
7 import logging
8 from typing import Any
9 
10 from universal_silabs_flasher.const import ApplicationType
11 
13  AddonError,
14  AddonInfo,
15  AddonManager,
16  AddonState,
17 )
19  probe_silabs_firmware_type,
20 )
21 from homeassistant.config_entries import (
22  ConfigEntry,
23  ConfigEntryBaseFlow,
24  ConfigFlow,
25  ConfigFlowResult,
26  OptionsFlow,
27 )
28 from homeassistant.core import callback
29 from homeassistant.data_entry_flow import AbortFlow
30 from homeassistant.helpers.hassio import is_hassio
31 
32 from . import silabs_multiprotocol_addon
33 from .const import ZHA_DOMAIN
34 from .util import (
35  get_otbr_addon_manager,
36  get_zha_device_path,
37  get_zigbee_flasher_addon_manager,
38 )
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread"
43 STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee"
44 
45 
47  """Base flow to install firmware."""
48 
49  _failed_addon_name: str
50  _failed_addon_reason: str
51 
52  def __init__(self, *args: Any, **kwargs: Any) -> None:
53  """Instantiate base flow."""
54  super().__init__(*args, **kwargs)
55 
56  self._probed_firmware_type_probed_firmware_type: ApplicationType | None = None
57  self._device: str | None = None # To be set in a subclass
58  self._hardware_name: str = "unknown" # To be set in a subclass
59 
60  self.addon_install_taskaddon_install_task: asyncio.Task | None = None
61  self.addon_start_taskaddon_start_task: asyncio.Task | None = None
62  self.addon_uninstall_taskaddon_uninstall_task: asyncio.Task | None = None
63 
64  def _get_translation_placeholders(self) -> dict[str, str]:
65  """Shared translation placeholders."""
66  placeholders = {
67  "firmware_type": (
68  self._probed_firmware_type_probed_firmware_type.value
69  if self._probed_firmware_type_probed_firmware_type is not None
70  else "unknown"
71  ),
72  "model": self._hardware_name,
73  }
74 
75  self.context["title_placeholders"] = placeholders
76 
77  return placeholders
78 
80  self, config: dict, addon_manager: AddonManager
81  ) -> None:
82  """Set add-on config."""
83  try:
84  await addon_manager.async_set_addon_options(config)
85  except AddonError as err:
86  _LOGGER.error(err)
87  raise AbortFlow(
88  "addon_set_config_failed",
89  description_placeholders={
90  **self._get_translation_placeholders_get_translation_placeholders(),
91  "addon_name": addon_manager.addon_name,
92  },
93  ) from err
94 
95  async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo:
96  """Return add-on info."""
97  try:
98  addon_info = await addon_manager.async_get_addon_info()
99  except AddonError as err:
100  _LOGGER.error(err)
101  raise AbortFlow(
102  "addon_info_failed",
103  description_placeholders={
104  **self._get_translation_placeholders_get_translation_placeholders(),
105  "addon_name": addon_manager.addon_name,
106  },
107  ) from err
108 
109  return addon_info
110 
112  self, user_input: dict[str, Any] | None = None
113  ) -> ConfigFlowResult:
114  """Pick Thread or Zigbee firmware."""
115  return self.async_show_menuasync_show_menu(
116  step_id="pick_firmware",
117  menu_options=[
118  STEP_PICK_FIRMWARE_ZIGBEE,
119  STEP_PICK_FIRMWARE_THREAD,
120  ],
121  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
122  )
123 
124  async def _probe_firmware_type(self) -> bool:
125  """Probe the firmware currently on the device."""
126  assert self._device is not None
127 
128  self._probed_firmware_type_probed_firmware_type = await probe_silabs_firmware_type(
129  self._device,
130  probe_methods=(
131  # We probe in order of frequency: Zigbee, Thread, then multi-PAN
132  ApplicationType.GECKO_BOOTLOADER,
133  ApplicationType.EZSP,
134  ApplicationType.SPINEL,
135  ApplicationType.CPC,
136  ),
137  )
138 
139  return self._probed_firmware_type_probed_firmware_type in (
140  ApplicationType.EZSP,
141  ApplicationType.SPINEL,
142  ApplicationType.CPC,
143  )
144 
146  self, user_input: dict[str, Any] | None = None
147  ) -> ConfigFlowResult:
148  """Pick Zigbee firmware."""
149  if not await self._probe_firmware_type_probe_firmware_type():
150  return self.async_abortasync_abort(
151  reason="unsupported_firmware",
152  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
153  )
154 
155  # Allow the stick to be used with ZHA without flashing
156  if self._probed_firmware_type_probed_firmware_type == ApplicationType.EZSP:
157  return await self.async_step_confirm_zigbeeasync_step_confirm_zigbee()
158 
159  if not is_hassio(self.hass):
160  return self.async_abortasync_abort(
161  reason="not_hassio",
162  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
163  )
164 
165  # Only flash new firmware if we need to
166  fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
167  addon_info = await self._async_get_addon_info_async_get_addon_info(fw_flasher_manager)
168 
169  if addon_info.state == AddonState.NOT_INSTALLED:
170  return await self.async_step_install_zigbee_flasher_addonasync_step_install_zigbee_flasher_addon()
171 
172  if addon_info.state == AddonState.NOT_RUNNING:
173  return await self.async_step_run_zigbee_flasher_addonasync_step_run_zigbee_flasher_addon()
174 
175  # If the addon is already installed and running, fail
176  return self.async_abortasync_abort(
177  reason="addon_already_running",
178  description_placeholders={
179  **self._get_translation_placeholders_get_translation_placeholders(),
180  "addon_name": fw_flasher_manager.addon_name,
181  },
182  )
183 
185  self, user_input: dict[str, Any] | None = None
186  ) -> ConfigFlowResult:
187  """Show progress dialog for installing the Zigbee flasher addon."""
188  return await self._install_addon_install_addon(
190  "install_zigbee_flasher_addon",
191  "run_zigbee_flasher_addon",
192  )
193 
194  async def _install_addon(
195  self,
196  addon_manager: silabs_multiprotocol_addon.WaitingAddonManager,
197  step_id: str,
198  next_step_id: str,
199  ) -> ConfigFlowResult:
200  """Show progress dialog for installing an addon."""
201  addon_info = await self._async_get_addon_info_async_get_addon_info(addon_manager)
202 
203  _LOGGER.debug("Flasher addon state: %s", addon_info)
204 
205  if not self.addon_install_taskaddon_install_task:
206  self.addon_install_taskaddon_install_task = self.hass.async_create_task(
207  addon_manager.async_install_addon_waiting(),
208  "Addon install",
209  )
210 
211  if not self.addon_install_taskaddon_install_task.done():
212  return self.async_show_progressasync_show_progress(
213  step_id=step_id,
214  progress_action="install_addon",
215  description_placeholders={
216  **self._get_translation_placeholders_get_translation_placeholders(),
217  "addon_name": addon_manager.addon_name,
218  },
219  progress_task=self.addon_install_taskaddon_install_task,
220  )
221 
222  try:
223  await self.addon_install_taskaddon_install_task
224  except AddonError as err:
225  _LOGGER.error(err)
226  self._failed_addon_name_failed_addon_name = addon_manager.addon_name
227  self._failed_addon_reason_failed_addon_reason = "addon_install_failed"
228  return self.async_show_progress_doneasync_show_progress_done(next_step_id="addon_operation_failed")
229  finally:
230  self.addon_install_taskaddon_install_task = None
231 
232  return self.async_show_progress_doneasync_show_progress_done(next_step_id=next_step_id)
233 
235  self, user_input: dict[str, Any] | None = None
236  ) -> ConfigFlowResult:
237  """Abort when add-on installation or start failed."""
238  return self.async_abortasync_abort(
239  reason=self._failed_addon_reason_failed_addon_reason,
240  description_placeholders={
241  **self._get_translation_placeholders_get_translation_placeholders(),
242  "addon_name": self._failed_addon_name_failed_addon_name,
243  },
244  )
245 
247  self, user_input: dict[str, Any] | None = None
248  ) -> ConfigFlowResult:
249  """Configure the flasher addon to point to the SkyConnect and run it."""
250  fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
251  addon_info = await self._async_get_addon_info_async_get_addon_info(fw_flasher_manager)
252 
253  assert self._device is not None
254  new_addon_config = {
255  **addon_info.options,
256  "device": self._device,
257  "baudrate": 115200,
258  "bootloader_baudrate": 115200,
259  "flow_control": True,
260  }
261 
262  _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
263  await self._async_set_addon_config_async_set_addon_config(new_addon_config, fw_flasher_manager)
264 
265  if not self.addon_start_taskaddon_start_task:
266 
267  async def start_and_wait_until_done() -> None:
268  await fw_flasher_manager.async_start_addon_waiting()
269  # Now that the addon is running, wait for it to finish
270  await fw_flasher_manager.async_wait_until_addon_state(
271  AddonState.NOT_RUNNING
272  )
273 
274  self.addon_start_taskaddon_start_task = self.hass.async_create_task(
275  start_and_wait_until_done()
276  )
277 
278  if not self.addon_start_taskaddon_start_task.done():
279  return self.async_show_progressasync_show_progress(
280  step_id="run_zigbee_flasher_addon",
281  progress_action="run_zigbee_flasher_addon",
282  description_placeholders={
283  **self._get_translation_placeholders_get_translation_placeholders(),
284  "addon_name": fw_flasher_manager.addon_name,
285  },
286  progress_task=self.addon_start_taskaddon_start_task,
287  )
288 
289  try:
290  await self.addon_start_taskaddon_start_task
291  except (AddonError, AbortFlow) as err:
292  _LOGGER.error(err)
293  self._failed_addon_name_failed_addon_name = fw_flasher_manager.addon_name
294  self._failed_addon_reason_failed_addon_reason = "addon_start_failed"
295  return self.async_show_progress_doneasync_show_progress_done(next_step_id="addon_operation_failed")
296  finally:
297  self.addon_start_taskaddon_start_task = None
298 
299  return self.async_show_progress_doneasync_show_progress_done(
300  next_step_id="uninstall_zigbee_flasher_addon"
301  )
302 
304  self, user_input: dict[str, Any] | None = None
305  ) -> ConfigFlowResult:
306  """Uninstall the flasher addon."""
307  fw_flasher_manager = get_zigbee_flasher_addon_manager(self.hass)
308 
309  if not self.addon_uninstall_taskaddon_uninstall_task:
310  _LOGGER.debug("Uninstalling flasher addon")
311  self.addon_uninstall_taskaddon_uninstall_task = self.hass.async_create_task(
312  fw_flasher_manager.async_uninstall_addon_waiting()
313  )
314 
315  if not self.addon_uninstall_taskaddon_uninstall_task.done():
316  return self.async_show_progressasync_show_progress(
317  step_id="uninstall_zigbee_flasher_addon",
318  progress_action="uninstall_zigbee_flasher_addon",
319  description_placeholders={
320  **self._get_translation_placeholders_get_translation_placeholders(),
321  "addon_name": fw_flasher_manager.addon_name,
322  },
323  progress_task=self.addon_uninstall_taskaddon_uninstall_task,
324  )
325 
326  try:
327  await self.addon_uninstall_taskaddon_uninstall_task
328  except (AddonError, AbortFlow) as err:
329  _LOGGER.error(err)
330  # The uninstall failing isn't critical so we can just continue
331  finally:
332  self.addon_uninstall_taskaddon_uninstall_task = None
333 
334  return self.async_show_progress_doneasync_show_progress_done(next_step_id="confirm_zigbee")
335 
337  self, user_input: dict[str, Any] | None = None
338  ) -> ConfigFlowResult:
339  """Confirm Zigbee setup."""
340  assert self._device is not None
341  assert self._hardware_name is not None
342  self._probed_firmware_type_probed_firmware_type = ApplicationType.EZSP
343 
344  if user_input is not None:
345  await self.hass.config_entries.flow.async_init(
346  ZHA_DOMAIN,
347  context={"source": "hardware"},
348  data={
349  "name": self._hardware_name,
350  "port": {
351  "path": self._device,
352  "baudrate": 115200,
353  "flow_control": "hardware",
354  },
355  "radio_type": "ezsp",
356  },
357  )
358 
359  return self._async_flow_finished_async_flow_finished()
360 
361  return self.async_show_formasync_show_form(
362  step_id="confirm_zigbee",
363  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
364  )
365 
367  self, user_input: dict[str, Any] | None = None
368  ) -> ConfigFlowResult:
369  """Pick Thread firmware."""
370  if not await self._probe_firmware_type_probe_firmware_type():
371  return self.async_abortasync_abort(
372  reason="unsupported_firmware",
373  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
374  )
375 
376  # We install the OTBR addon no matter what, since it is required to use Thread
377  if not is_hassio(self.hass):
378  return self.async_abortasync_abort(
379  reason="not_hassio_thread",
380  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
381  )
382 
383  otbr_manager = get_otbr_addon_manager(self.hass)
384  addon_info = await self._async_get_addon_info_async_get_addon_info(otbr_manager)
385 
386  if addon_info.state == AddonState.NOT_INSTALLED:
387  return await self.async_step_install_otbr_addonasync_step_install_otbr_addon()
388 
389  if addon_info.state == AddonState.NOT_RUNNING:
390  return await self.async_step_start_otbr_addonasync_step_start_otbr_addon()
391 
392  # If the addon is already installed and running, fail
393  return self.async_abortasync_abort(
394  reason="otbr_addon_already_running",
395  description_placeholders={
396  **self._get_translation_placeholders_get_translation_placeholders(),
397  "addon_name": otbr_manager.addon_name,
398  },
399  )
400 
402  self, user_input: dict[str, Any] | None = None
403  ) -> ConfigFlowResult:
404  """Show progress dialog for installing the OTBR addon."""
405  return await self._install_addon_install_addon(
406  get_otbr_addon_manager(self.hass), "install_otbr_addon", "start_otbr_addon"
407  )
408 
410  self, user_input: dict[str, Any] | None = None
411  ) -> ConfigFlowResult:
412  """Configure OTBR to point to the SkyConnect and run the addon."""
413  otbr_manager = get_otbr_addon_manager(self.hass)
414  addon_info = await self._async_get_addon_info_async_get_addon_info(otbr_manager)
415 
416  assert self._device is not None
417  new_addon_config = {
418  **addon_info.options,
419  "device": self._device,
420  "baudrate": 460800,
421  "flow_control": True,
422  "autoflash_firmware": True,
423  }
424 
425  _LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config)
426  await self._async_set_addon_config_async_set_addon_config(new_addon_config, otbr_manager)
427 
428  if not self.addon_start_taskaddon_start_task:
429  self.addon_start_taskaddon_start_task = self.hass.async_create_task(
430  otbr_manager.async_start_addon_waiting()
431  )
432 
433  if not self.addon_start_taskaddon_start_task.done():
434  return self.async_show_progressasync_show_progress(
435  step_id="start_otbr_addon",
436  progress_action="start_otbr_addon",
437  description_placeholders={
438  **self._get_translation_placeholders_get_translation_placeholders(),
439  "addon_name": otbr_manager.addon_name,
440  },
441  progress_task=self.addon_start_taskaddon_start_task,
442  )
443 
444  try:
445  await self.addon_start_taskaddon_start_task
446  except (AddonError, AbortFlow) as err:
447  _LOGGER.error(err)
448  self._failed_addon_name_failed_addon_name = otbr_manager.addon_name
449  self._failed_addon_reason_failed_addon_reason = "addon_start_failed"
450  return self.async_show_progress_doneasync_show_progress_done(next_step_id="addon_operation_failed")
451  finally:
452  self.addon_start_taskaddon_start_task = None
453 
454  return self.async_show_progress_doneasync_show_progress_done(next_step_id="confirm_otbr")
455 
457  self, user_input: dict[str, Any] | None = None
458  ) -> ConfigFlowResult:
459  """Confirm OTBR setup."""
460  assert self._device is not None
461 
462  self._probed_firmware_type_probed_firmware_type = ApplicationType.SPINEL
463 
464  if user_input is not None:
465  # OTBR discovery is done automatically via hassio
466  return self._async_flow_finished_async_flow_finished()
467 
468  return self.async_show_formasync_show_form(
469  step_id="confirm_otbr",
470  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
471  )
472 
473  @abstractmethod
474  def _async_flow_finished(self) -> ConfigFlowResult:
475  """Finish the flow."""
476  raise NotImplementedError
477 
478 
480  """Base config flow for installing firmware."""
481 
482  @staticmethod
483  @callback
484  @abstractmethod
486  config_entry: ConfigEntry,
487  ) -> OptionsFlow:
488  """Return the options flow."""
489  raise NotImplementedError
490 
492  self, user_input: dict[str, Any] | None = None
493  ) -> ConfigFlowResult:
494  """Confirm a discovery."""
495  return await self.async_step_pick_firmwareasync_step_pick_firmware()
496 
497 
499  """Zigbee and Thread options flow handlers."""
500 
501  def __init__(self, config_entry: ConfigEntry, *args: Any, **kwargs: Any) -> None:
502  """Instantiate options flow."""
503  super().__init__(*args, **kwargs)
504 
505  self._config_entry_config_entry_config_entry = config_entry
506 
507  self._probed_firmware_type_probed_firmware_type_probed_firmware_type = ApplicationType(self.config_entryconfig_entryconfig_entry.data["firmware"])
508 
509  # Make `context` a regular dictionary
510  self.contextcontext = {}
511 
512  # Subclasses are expected to override `_device` and `_hardware_name`
513 
514  async def async_step_init(
515  self, user_input: dict[str, Any] | None = None
516  ) -> ConfigFlowResult:
517  """Manage the options flow."""
518  return await self.async_step_pick_firmwareasync_step_pick_firmware()
519 
521  self, user_input: dict[str, Any] | None = None
522  ) -> ConfigFlowResult:
523  """Pick Zigbee firmware."""
524  assert self._device is not None
525 
526  if is_hassio(self.hass):
527  otbr_manager = get_otbr_addon_manager(self.hass)
528  otbr_addon_info = await self._async_get_addon_info_async_get_addon_info(otbr_manager)
529 
530  if (
531  otbr_addon_info.state != AddonState.NOT_INSTALLED
532  and otbr_addon_info.options.get("device") == self._device
533  ):
534  raise AbortFlow(
535  "otbr_still_using_stick",
536  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
537  )
538 
539  return await super().async_step_pick_firmware_zigbee(user_input)
540 
542  self, user_input: dict[str, Any] | None = None
543  ) -> ConfigFlowResult:
544  """Pick Thread firmware."""
545  assert self._device is not None
546 
547  for zha_entry in self.hass.config_entries.async_entries(
548  ZHA_DOMAIN,
549  include_ignore=False,
550  include_disabled=True,
551  ):
552  if get_zha_device_path(zha_entry) == self._device:
553  raise AbortFlow(
554  "zha_still_using_stick",
555  description_placeholders=self._get_translation_placeholders_get_translation_placeholders(),
556  )
557 
558  return await super().async_step_pick_firmware_thread(user_input)
ConfigFlowResult async_step_install_zigbee_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_run_zigbee_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_pick_firmware_zigbee(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_confirm_zigbee(self, dict[str, Any]|None user_input=None)
ConfigFlowResult _install_addon(self, silabs_multiprotocol_addon.WaitingAddonManager addon_manager, str step_id, str next_step_id)
ConfigFlowResult async_step_addon_operation_failed(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_start_otbr_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_pick_firmware(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_install_otbr_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_pick_firmware_thread(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_uninstall_zigbee_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_pick_firmware_thread(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_pick_firmware_zigbee(self, dict[str, Any]|None user_input=None)
None config_entry(self, ConfigEntry value)
_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_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
WaitingAddonManager get_zigbee_flasher_addon_manager(HomeAssistant hass)
Definition: util.py:54
WaitingAddonManager get_otbr_addon_manager(HomeAssistant hass)
Definition: util.py:42
str|None get_zha_device_path(ConfigEntry config_entry)
Definition: util.py:35
ApplicationType|None probe_silabs_firmware_type(str device, *ApplicationType|None probe_methods=None)