Home Assistant Unofficial Reference 2024.12.1
silabs_multiprotocol_addon.py
Go to the documentation of this file.
1 """Manage the Silicon Labs Multiprotocol add-on."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 import asyncio
7 import dataclasses
8 import logging
9 from typing import Any, Protocol
10 
11 import voluptuous as vol
12 import yarl
13 
15  AddonError,
16  AddonInfo,
17  AddonManager,
18  AddonState,
19  hostname_from_addon_slug,
20 )
21 from homeassistant.config_entries import (
22  ConfigEntry,
23  ConfigFlowResult,
24  OptionsFlow,
25  OptionsFlowManager,
26 )
27 from homeassistant.core import HomeAssistant, callback
28 from homeassistant.data_entry_flow import AbortFlow
29 from homeassistant.exceptions import HomeAssistantError
30 from homeassistant.helpers.hassio import is_hassio
32  async_process_integration_platforms,
33 )
35  SelectSelector,
36  SelectSelectorConfig,
37  SelectSelectorMode,
38 )
39 from homeassistant.helpers.singleton import singleton
40 from homeassistant.helpers.storage import Store
41 
42 from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
43 
44 _LOGGER = logging.getLogger(__name__)
45 
46 DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
47 DATA_FLASHER_ADDON_MANAGER = "silabs_flasher"
48 
49 ADDON_STATE_POLL_INTERVAL = 3
50 ADDON_INFO_POLL_TIMEOUT = 15 * 60
51 
52 CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
53 CONF_ADDON_DEVICE = "device"
54 CONF_DISABLE_MULTI_PAN = "disable_multi_pan"
55 CONF_ENABLE_MULTI_PAN = "enable_multi_pan"
56 
57 DEFAULT_CHANNEL = 15
58 DEFAULT_CHANNEL_CHANGE_DELAY = 5 * 60 # Thread recommendation
59 
60 STORAGE_KEY = "homeassistant_hardware.silabs"
61 STORAGE_VERSION_MAJOR = 1
62 STORAGE_VERSION_MINOR = 1
63 SAVE_DELAY = 10
64 
65 
66 @singleton(DATA_MULTIPROTOCOL_ADDON_MANAGER)
68  hass: HomeAssistant,
69 ) -> MultiprotocolAddonManager:
70  """Get the add-on manager."""
71  manager = MultiprotocolAddonManager(hass)
72  await manager.async_setup()
73  return manager
74 
75 
77  """Addon manager which supports waiting operations for managing an addon."""
78 
79  async def async_wait_until_addon_state(self, *states: AddonState) -> None:
80  """Poll an addon's info until it is in a specific state."""
81  async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
82  while True:
83  try:
84  info = await self.async_get_addon_infoasync_get_addon_info()
85  except AddonError:
86  info = None
87 
88  _LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
89 
90  if info is not None and info.state in states:
91  break
92 
93  await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
94 
95  async def async_start_addon_waiting(self) -> None:
96  """Start an add-on."""
97  await self.async_schedule_start_addonasync_schedule_start_addon()
98  await self.async_wait_until_addon_stateasync_wait_until_addon_state(AddonState.RUNNING)
99 
100  async def async_install_addon_waiting(self) -> None:
101  """Install an add-on."""
102  await self.async_schedule_install_addonasync_schedule_install_addon()
103  await self.async_wait_until_addon_stateasync_wait_until_addon_state(
104  AddonState.RUNNING,
105  AddonState.NOT_RUNNING,
106  )
107 
108  async def async_uninstall_addon_waiting(self) -> None:
109  """Uninstall an add-on."""
110  try:
111  info = await self.async_get_addon_infoasync_get_addon_info()
112  except AddonError:
113  info = None
114 
115  # Do not try to uninstall an addon if it is already uninstalled
116  if info is not None and info.state == AddonState.NOT_INSTALLED:
117  return
118 
119  await self.async_uninstall_addonasync_uninstall_addon()
120  await self.async_wait_until_addon_stateasync_wait_until_addon_state(AddonState.NOT_INSTALLED)
121 
122 
124  """Silicon Labs Multiprotocol add-on manager."""
125 
126  def __init__(self, hass: HomeAssistant) -> None:
127  """Initialize the manager."""
128  super().__init__(
129  hass,
130  LOGGER,
131  "Silicon Labs Multiprotocol",
132  SILABS_MULTIPROTOCOL_ADDON_SLUG,
133  )
134  self._channel_channel: int | None = None
135  self._platforms: dict[str, MultipanProtocol] = {}
136  self._store: Store[dict[str, Any]] = Store(
137  hass,
138  STORAGE_VERSION_MAJOR,
139  STORAGE_KEY,
140  atomic_writes=True,
141  minor_version=STORAGE_VERSION_MINOR,
142  )
143 
144  async def async_setup(self) -> None:
145  """Set up the manager."""
147  self._hass_hass,
148  "silabs_multiprotocol",
149  self._register_multipan_platform_register_multipan_platform,
150  wait_for_platforms=True,
151  )
152  await self.async_loadasync_load()
153 
155  self, hass: HomeAssistant, integration_domain: str, platform: MultipanProtocol
156  ) -> None:
157  """Register a multipan platform."""
158  self._platforms[integration_domain] = platform
159 
160  channel = await platform.async_get_channel(hass)
161  using_multipan = await platform.async_using_multipan(hass)
162 
163  _LOGGER.info(
164  "Registering new multipan platform '%s', using multipan: %s, channel: %s",
165  integration_domain,
166  using_multipan,
167  channel,
168  )
169 
170  if self._channel_channel is not None or not using_multipan:
171  return
172 
173  if channel is None:
174  return
175 
176  _LOGGER.info(
177  "Setting multipan channel to %s (source: '%s')",
178  channel,
179  integration_domain,
180  )
181  self.async_set_channelasync_set_channel(channel)
182 
184  self, channel: int, delay: float
185  ) -> list[asyncio.Task]:
186  """Change the channel and notify platforms."""
187  self.async_set_channelasync_set_channel(channel)
188 
189  tasks = []
190 
191  for platform in self._platforms.values():
192  if not await platform.async_using_multipan(self._hass_hass):
193  continue
194  task = await platform.async_change_channel(self._hass_hass, channel, delay)
195  if not task:
196  continue
197  tasks.append(task)
198 
199  return tasks
200 
201  async def async_active_platforms(self) -> list[str]:
202  """Return a list of platforms using the multipan radio."""
203  active_platforms: list[str] = []
204 
205  for integration_domain, platform in self._platforms.items():
206  if not await platform.async_using_multipan(self._hass_hass):
207  continue
208  active_platforms.append(integration_domain)
209 
210  return active_platforms
211 
212  @callback
213  def async_get_channel(self) -> int | None:
214  """Get the channel."""
215  return self._channel_channel
216 
217  @callback
218  def async_set_channel(self, channel: int) -> None:
219  """Set the channel without notifying platforms.
220 
221  This must only be called when first initializing the manager.
222  """
223  self._channel_channel = channel
224  self.async_schedule_saveasync_schedule_save()
225 
226  async def async_load(self) -> None:
227  """Load the store."""
228  data = await self._store.async_load()
229 
230  if data is not None:
231  self._channel_channel = data["channel"]
232 
233  @callback
234  def async_schedule_save(self) -> None:
235  """Schedule saving the store."""
236  self._store.async_delay_save(self._data_to_save_data_to_save, SAVE_DELAY)
237 
238  @callback
239  def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]:
240  """Return data to store in a file."""
241  data: dict[str, Any] = {}
242  data["channel"] = self._channel_channel
243  return data
244 
245 
246 class MultipanProtocol(Protocol):
247  """Define the format of multipan platforms."""
248 
250  self, hass: HomeAssistant, channel: int, delay: float
251  ) -> asyncio.Task | None:
252  """Set the channel to be used.
253 
254  Does nothing if not configured or the multiprotocol add-on is not used.
255  """
256 
257  async def async_get_channel(self, hass: HomeAssistant) -> int | None:
258  """Return the channel.
259 
260  Returns None if not configured or the multiprotocol add-on is not used.
261  """
262 
263  async def async_using_multipan(self, hass: HomeAssistant) -> bool:
264  """Return if the multiprotocol device is used.
265 
266  Returns False if not configured.
267  """
268 
269 
270 @singleton(DATA_FLASHER_ADDON_MANAGER)
271 @callback
272 def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
273  """Get the flasher add-on manager."""
274  return WaitingAddonManager(
275  hass,
276  LOGGER,
277  "Silicon Labs Flasher",
278  SILABS_FLASHER_ADDON_SLUG,
279  )
280 
281 
282 @dataclasses.dataclass
284  """Serial port settings."""
285 
286  device: str
287  baudrate: str
288  flow_control: bool
289 
290 
291 def get_zigbee_socket() -> str:
292  """Return the zigbee socket.
293 
294  Raises AddonError on error
295  """
296  hostname = hostname_from_addon_slug(SILABS_MULTIPROTOCOL_ADDON_SLUG)
297  return f"socket://{hostname}:9999"
298 
299 
300 def is_multiprotocol_url(url: str) -> bool:
301  """Return if the URL points at the Multiprotocol add-on."""
302  parsed = yarl.URL(url)
303  hostname = hostname_from_addon_slug(SILABS_MULTIPROTOCOL_ADDON_SLUG)
304  return parsed.host == hostname
305 
306 
308  """Handle an options flow for the Silicon Labs Multiprotocol add-on."""
309 
310  def __init__(self, config_entry: ConfigEntry) -> None:
311  """Set up the options flow."""
312  # pylint: disable-next=import-outside-toplevel
314  ZhaMultiPANMigrationHelper,
315  )
316 
317  self.install_taskinstall_task: asyncio.Task | None = None
318  self.start_taskstart_task: asyncio.Task | None = None
319  self.stop_taskstop_task: asyncio.Task | None = None
320  self._zha_migration_mgr_zha_migration_mgr: ZhaMultiPANMigrationHelper | None = None
321  self.original_addon_configoriginal_addon_config: dict[str, Any] | None = None
322  self.revert_reason: str | None = None
323 
324  @abstractmethod
325  async def _async_serial_port_settings(self) -> SerialPortSettings:
326  """Return the radio serial port settings."""
327 
328  @abstractmethod
329  async def _async_zha_physical_discovery(self) -> dict[str, Any]:
330  """Return ZHA discovery data when multiprotocol FW is not used.
331 
332  Passed to ZHA do determine if the ZHA config entry is connected to the radio
333  being migrated.
334  """
335 
336  @abstractmethod
337  def _hardware_name(self) -> str:
338  """Return the name of the hardware."""
339 
340  @abstractmethod
341  def _zha_name(self) -> str:
342  """Return the ZHA name."""
343 
344  @property
345  def flow_manager(self) -> OptionsFlowManager:
346  """Return the correct flow manager."""
347  return self.hass.config_entries.options
348 
349  async def _async_get_addon_info(self, addon_manager: AddonManager) -> AddonInfo:
350  """Return and cache Silicon Labs Multiprotocol add-on info."""
351  try:
352  addon_info: AddonInfo = await addon_manager.async_get_addon_info()
353  except AddonError as err:
354  _LOGGER.error(err)
355  raise AbortFlow(
356  "addon_info_failed",
357  description_placeholders={"addon_name": addon_manager.addon_name},
358  ) from err
359 
360  return addon_info
361 
363  self, config: dict, addon_manager: AddonManager
364  ) -> None:
365  """Set Silicon Labs Multiprotocol add-on config."""
366  try:
367  await addon_manager.async_set_addon_options(config)
368  except AddonError as err:
369  _LOGGER.error(err)
370  raise AbortFlow("addon_set_config_failed") from err
371 
372  async def async_step_init(
373  self, user_input: dict[str, Any] | None = None
374  ) -> ConfigFlowResult:
375  """Manage the options."""
376  if not is_hassio(self.hass):
377  return self.async_abortasync_abort(reason="not_hassio")
378 
379  return await self.async_step_on_supervisorasync_step_on_supervisor()
380 
382  self, user_input: dict[str, Any] | None = None
383  ) -> ConfigFlowResult:
384  """Handle logic when on Supervisor host."""
385  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
386  addon_info = await self._async_get_addon_info_async_get_addon_info(multipan_manager)
387 
388  if addon_info.state == AddonState.NOT_INSTALLED:
389  return await self.async_step_addon_not_installedasync_step_addon_not_installed()
390  return await self.async_step_addon_installedasync_step_addon_installed()
391 
393  self, user_input: dict[str, Any] | None = None
394  ) -> ConfigFlowResult:
395  """Handle logic when the addon is not yet installed."""
396  if user_input is None:
397  return self.async_show_formasync_show_form(
398  step_id="addon_not_installed",
399  data_schema=vol.Schema(
400  {vol.Required(CONF_ENABLE_MULTI_PAN, default=False): bool}
401  ),
402  description_placeholders={"hardware_name": self._hardware_name_hardware_name()},
403  )
404  if not user_input[CONF_ENABLE_MULTI_PAN]:
405  return self.async_create_entryasync_create_entry(title="", data={})
406 
407  return await self.async_step_install_addonasync_step_install_addon()
408 
410  self, user_input: dict[str, Any] | None = None
411  ) -> ConfigFlowResult:
412  """Install Silicon Labs Multiprotocol add-on."""
413  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
414 
415  if not self.install_taskinstall_task:
416  self.install_taskinstall_task = self.hass.async_create_task(
417  multipan_manager.async_install_addon_waiting(),
418  "SiLabs Multiprotocol addon install",
419  eager_start=False,
420  )
421 
422  if not self.install_taskinstall_task.done():
423  return self.async_show_progressasync_show_progress(
424  step_id="install_addon",
425  progress_action="install_addon",
426  description_placeholders={"addon_name": multipan_manager.addon_name},
427  progress_task=self.install_taskinstall_task,
428  )
429 
430  try:
431  await self.install_taskinstall_task
432  except AddonError as err:
433  _LOGGER.error(err)
434  return self.async_show_progress_doneasync_show_progress_done(next_step_id="install_failed")
435  finally:
436  self.install_taskinstall_task = None
437 
438  return self.async_show_progress_doneasync_show_progress_done(next_step_id="configure_addon")
439 
441  self, user_input: dict[str, Any] | None = None
442  ) -> ConfigFlowResult:
443  """Add-on installation failed."""
444  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
445  return self.async_abortasync_abort(
446  reason="addon_install_failed",
447  description_placeholders={"addon_name": multipan_manager.addon_name},
448  )
449 
451  self, user_input: dict[str, Any] | None = None
452  ) -> ConfigFlowResult:
453  """Configure the Silicon Labs Multiprotocol add-on."""
454  # pylint: disable-next=import-outside-toplevel
455  from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
456 
457  # pylint: disable-next=import-outside-toplevel
459  ZhaMultiPANMigrationHelper,
460  )
461 
462  # pylint: disable-next=import-outside-toplevel
464  async_get_channel as async_get_zha_channel,
465  )
466 
467  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
468  addon_info = await self._async_get_addon_info_async_get_addon_info(multipan_manager)
469 
470  addon_config = addon_info.options
471 
472  serial_port_settings = await self._async_serial_port_settings_async_serial_port_settings()
473  new_addon_config = {
474  **addon_config,
475  CONF_ADDON_AUTOFLASH_FW: True,
476  **dataclasses.asdict(serial_port_settings),
477  }
478 
479  multipan_channel = DEFAULT_CHANNEL
480 
481  # Initiate ZHA migration
482  zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
483 
484  if zha_entries:
485  zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0])
486  migration_data = {
487  "new_discovery_info": {
488  "name": self._zha_name_zha_name(),
489  "port": {
490  "path": get_zigbee_socket(),
491  },
492  "radio_type": "ezsp",
493  },
494  "old_discovery_info": await self._async_zha_physical_discovery_async_zha_physical_discovery(),
495  }
496  _LOGGER.debug("Starting ZHA migration with: %s", migration_data)
497  try:
498  if await zha_migration_mgr.async_initiate_migration(migration_data):
499  self._zha_migration_mgr_zha_migration_mgr = zha_migration_mgr
500  except Exception as err:
501  _LOGGER.exception("Unexpected exception during ZHA migration")
502  raise AbortFlow("zha_migration_failed") from err
503 
504  if (zha_channel := await async_get_zha_channel(self.hass)) is not None:
505  multipan_channel = zha_channel
506 
507  # Initialize the shared channel
508  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
509  multipan_manager.async_set_channel(multipan_channel)
510 
511  if new_addon_config != addon_config:
512  # Copy the add-on config to keep the objects separate.
513  self.original_addon_configoriginal_addon_config = dict(addon_config)
514  _LOGGER.debug("Reconfiguring addon with %s", new_addon_config)
515  await self._async_set_addon_config_async_set_addon_config(new_addon_config, multipan_manager)
516 
517  return await self.async_step_start_addonasync_step_start_addon()
518 
520  self, user_input: dict[str, Any] | None = None
521  ) -> ConfigFlowResult:
522  """Start Silicon Labs Multiprotocol add-on."""
523  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
524 
525  if not self.start_taskstart_task:
526  self.start_taskstart_task = self.hass.async_create_task(
527  multipan_manager.async_start_addon_waiting(), eager_start=False
528  )
529 
530  if not self.start_taskstart_task.done():
531  return self.async_show_progressasync_show_progress(
532  step_id="start_addon",
533  progress_action="start_addon",
534  description_placeholders={"addon_name": multipan_manager.addon_name},
535  progress_task=self.start_taskstart_task,
536  )
537 
538  try:
539  await self.start_taskstart_task
540  except (AddonError, AbortFlow) as err:
541  _LOGGER.error(err)
542  return self.async_show_progress_doneasync_show_progress_done(next_step_id="start_failed")
543  finally:
544  self.start_taskstart_task = None
545 
546  return self.async_show_progress_doneasync_show_progress_done(next_step_id="finish_addon_setup")
547 
549  self, user_input: dict[str, Any] | None = None
550  ) -> ConfigFlowResult:
551  """Add-on start failed."""
552  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
553  return self.async_abortasync_abort(
554  reason="addon_start_failed",
555  description_placeholders={"addon_name": multipan_manager.addon_name},
556  )
557 
559  self, user_input: dict[str, Any] | None = None
560  ) -> ConfigFlowResult:
561  """Prepare info needed to complete the config entry update."""
562  # Always reload entry after installing the addon.
563  self.hass.async_create_task(
564  self.hass.config_entries.async_reload(self.config_entryconfig_entryconfig_entry.entry_id),
565  eager_start=False,
566  )
567 
568  # Finish ZHA migration if needed
569  if self._zha_migration_mgr_zha_migration_mgr:
570  try:
571  await self._zha_migration_mgr_zha_migration_mgr.async_finish_migration()
572  except Exception as err:
573  _LOGGER.exception("Unexpected exception during ZHA migration")
574  raise AbortFlow("zha_migration_failed") from err
575 
576  return self.async_create_entryasync_create_entry(title="", data={})
577 
579  self, user_input: dict[str, Any] | None = None
580  ) -> ConfigFlowResult:
581  """Show dialog explaining the addon is in use by another device."""
582  if user_input is None:
583  return self.async_show_formasync_show_form(step_id="addon_installed_other_device")
584  return self.async_create_entryasync_create_entry(title="", data={})
585 
587  self, user_input: dict[str, Any] | None = None
588  ) -> ConfigFlowResult:
589  """Handle logic when the addon is already installed."""
590  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
591  addon_info = await self._async_get_addon_info_async_get_addon_info(multipan_manager)
592 
593  serial_device = (await self._async_serial_port_settings_async_serial_port_settings()).device
594  if addon_info.options.get(CONF_ADDON_DEVICE) != serial_device:
595  return await self.async_step_addon_installed_other_deviceasync_step_addon_installed_other_device()
596  return await self.async_step_addon_menuasync_step_addon_menu()
597 
599  self, user_input: dict[str, Any] | None = None
600  ) -> ConfigFlowResult:
601  """Show menu options for the addon."""
602  return self.async_show_menuasync_show_menu(
603  step_id="addon_menu",
604  menu_options=[
605  "reconfigure_addon",
606  "uninstall_addon",
607  ],
608  )
609 
611  self, user_input: dict[str, Any] | None = None
612  ) -> ConfigFlowResult:
613  """Reconfigure the addon."""
614  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
615  active_platforms = await multipan_manager.async_active_platforms()
616  if set(active_platforms) != {"otbr", "zha"}:
617  return await self.async_step_notify_unknown_multipan_userasync_step_notify_unknown_multipan_user()
618  return await self.async_step_change_channelasync_step_change_channel()
619 
621  self, user_input: dict[str, Any] | None = None
622  ) -> ConfigFlowResult:
623  """Notify that there may be unknown multipan platforms."""
624  if user_input is None:
625  return self.async_show_formasync_show_form(
626  step_id="notify_unknown_multipan_user",
627  )
628  return await self.async_step_change_channelasync_step_change_channel()
629 
631  self, user_input: dict[str, Any] | None = None
632  ) -> ConfigFlowResult:
633  """Change the channel."""
634  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
635  if user_input is None:
636  channels = [str(x) for x in range(11, 27)]
637  suggested_channel = DEFAULT_CHANNEL
638  if (channel := multipan_manager.async_get_channel()) is not None:
639  suggested_channel = channel
640  data_schema = vol.Schema(
641  {
642  vol.Required(
643  "channel",
644  description={"suggested_value": str(suggested_channel)},
645  ): SelectSelector(
647  options=channels, mode=SelectSelectorMode.DROPDOWN
648  )
649  )
650  }
651  )
652  return self.async_show_formasync_show_form(
653  step_id="change_channel", data_schema=data_schema
654  )
655 
656  # Change the shared channel
657  await multipan_manager.async_change_channel(
658  int(user_input["channel"]), DEFAULT_CHANNEL_CHANGE_DELAY
659  )
660  return await self.async_step_notify_channel_changeasync_step_notify_channel_change()
661 
663  self, user_input: dict[str, Any] | None = None
664  ) -> ConfigFlowResult:
665  """Notify that the channel change will take about five minutes."""
666  if user_input is None:
667  return self.async_show_formasync_show_form(
668  step_id="notify_channel_change",
669  description_placeholders={
670  "delay_minutes": str(DEFAULT_CHANNEL_CHANGE_DELAY // 60)
671  },
672  )
673  return self.async_create_entryasync_create_entry(title="", data={})
674 
676  self, user_input: dict[str, Any] | None = None
677  ) -> ConfigFlowResult:
678  """Uninstall the addon and revert the firmware."""
679  if user_input is None:
680  return self.async_show_formasync_show_form(
681  step_id="uninstall_addon",
682  data_schema=vol.Schema(
683  {vol.Required(CONF_DISABLE_MULTI_PAN, default=False): bool}
684  ),
685  description_placeholders={"hardware_name": self._hardware_name_hardware_name()},
686  )
687  if not user_input[CONF_DISABLE_MULTI_PAN]:
688  return self.async_create_entryasync_create_entry(title="", data={})
689 
690  return await self.async_step_firmware_revertasync_step_firmware_revert()
691 
693  self, user_input: dict[str, Any] | None = None
694  ) -> ConfigFlowResult:
695  """Install the flasher addon, if necessary."""
696 
697  flasher_manager = get_flasher_addon_manager(self.hass)
698  addon_info = await self._async_get_addon_info_async_get_addon_info(flasher_manager)
699 
700  if addon_info.state == AddonState.NOT_INSTALLED:
701  return await self.async_step_install_flasher_addonasync_step_install_flasher_addon()
702 
703  if addon_info.state == AddonState.NOT_RUNNING:
704  return await self.async_step_configure_flasher_addonasync_step_configure_flasher_addon()
705 
706  # If the addon is already installed and running, fail
707  return self.async_abortasync_abort(
708  reason="addon_already_running",
709  description_placeholders={"addon_name": flasher_manager.addon_name},
710  )
711 
713  self, user_input: dict[str, Any] | None = None
714  ) -> ConfigFlowResult:
715  """Show progress dialog for installing flasher addon."""
716  flasher_manager = get_flasher_addon_manager(self.hass)
717  addon_info = await self._async_get_addon_info_async_get_addon_info(flasher_manager)
718 
719  _LOGGER.debug("Flasher addon state: %s", addon_info)
720 
721  if not self.install_taskinstall_task:
722  self.install_taskinstall_task = self.hass.async_create_task(
723  flasher_manager.async_install_addon_waiting(),
724  "SiLabs Flasher addon install",
725  eager_start=False,
726  )
727 
728  if not self.install_taskinstall_task.done():
729  return self.async_show_progressasync_show_progress(
730  step_id="install_flasher_addon",
731  progress_action="install_addon",
732  description_placeholders={"addon_name": flasher_manager.addon_name},
733  progress_task=self.install_taskinstall_task,
734  )
735 
736  try:
737  await self.install_taskinstall_task
738  except AddonError as err:
739  _LOGGER.error(err)
740  return self.async_show_progress_doneasync_show_progress_done(next_step_id="install_failed")
741  finally:
742  self.install_taskinstall_task = None
743 
744  return self.async_show_progress_doneasync_show_progress_done(next_step_id="configure_flasher_addon")
745 
747  self, user_input: dict[str, Any] | None = None
748  ) -> ConfigFlowResult:
749  """Perform initial backup and reconfigure ZHA."""
750  # pylint: disable-next=import-outside-toplevel
751  from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
752 
753  # pylint: disable-next=import-outside-toplevel
755  ZhaMultiPANMigrationHelper,
756  )
757 
758  zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
759  new_settings = await self._async_serial_port_settings_async_serial_port_settings()
760 
761  _LOGGER.debug("Using new ZHA settings: %s", new_settings)
762 
763  if zha_entries:
764  zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0])
765  migration_data = {
766  "new_discovery_info": {
767  "name": self._hardware_name_hardware_name(),
768  "port": {
769  "path": new_settings.device,
770  "baudrate": int(new_settings.baudrate),
771  "flow_control": (
772  "hardware" if new_settings.flow_control else None
773  ),
774  },
775  "radio_type": "ezsp",
776  },
777  "old_discovery_info": {
778  "hw": {
779  "name": self._zha_name_zha_name(),
780  "port": {"path": get_zigbee_socket()},
781  "radio_type": "ezsp",
782  }
783  },
784  }
785  _LOGGER.debug("Starting ZHA migration with: %s", migration_data)
786  try:
787  if await zha_migration_mgr.async_initiate_migration(migration_data):
788  self._zha_migration_mgr_zha_migration_mgr = zha_migration_mgr
789  except Exception as err:
790  _LOGGER.exception("Unexpected exception during ZHA migration")
791  raise AbortFlow("zha_migration_failed") from err
792 
793  flasher_manager = get_flasher_addon_manager(self.hass)
794  addon_info = await self._async_get_addon_info_async_get_addon_info(flasher_manager)
795  new_addon_config = {
796  **addon_info.options,
797  "device": new_settings.device,
798  "flow_control": new_settings.flow_control,
799  }
800 
801  _LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
802  await self._async_set_addon_config_async_set_addon_config(new_addon_config, flasher_manager)
803 
804  return await self.async_step_uninstall_multiprotocol_addonasync_step_uninstall_multiprotocol_addon()
805 
807  self, user_input: dict[str, Any] | None = None
808  ) -> ConfigFlowResult:
809  """Uninstall Silicon Labs Multiprotocol add-on."""
810  multipan_manager = await get_multiprotocol_addon_manager(self.hass)
811 
812  if not self.stop_taskstop_task:
813  self.stop_taskstop_task = self.hass.async_create_task(
814  multipan_manager.async_uninstall_addon_waiting(),
815  "SiLabs Multiprotocol addon uninstall",
816  eager_start=False,
817  )
818 
819  if not self.stop_taskstop_task.done():
820  return self.async_show_progressasync_show_progress(
821  step_id="uninstall_multiprotocol_addon",
822  progress_action="uninstall_multiprotocol_addon",
823  description_placeholders={"addon_name": multipan_manager.addon_name},
824  progress_task=self.stop_taskstop_task,
825  )
826 
827  try:
828  await self.stop_taskstop_task
829  finally:
830  self.stop_taskstop_task = None
831 
832  return self.async_show_progress_doneasync_show_progress_done(next_step_id="start_flasher_addon")
833 
835  self, user_input: dict[str, Any] | None = None
836  ) -> ConfigFlowResult:
837  """Start Silicon Labs Flasher add-on."""
838  flasher_manager = get_flasher_addon_manager(self.hass)
839 
840  if not self.start_taskstart_task:
841 
842  async def start_and_wait_until_done() -> None:
843  await flasher_manager.async_start_addon_waiting()
844  # Now that the addon is running, wait for it to finish
845  await flasher_manager.async_wait_until_addon_state(
846  AddonState.NOT_RUNNING
847  )
848 
849  self.start_taskstart_task = self.hass.async_create_task(
850  start_and_wait_until_done(), eager_start=False
851  )
852 
853  if not self.start_taskstart_task.done():
854  return self.async_show_progressasync_show_progress(
855  step_id="start_flasher_addon",
856  progress_action="start_flasher_addon",
857  description_placeholders={"addon_name": flasher_manager.addon_name},
858  progress_task=self.start_taskstart_task,
859  )
860 
861  try:
862  await self.start_taskstart_task
863  except (AddonError, AbortFlow) as err:
864  _LOGGER.error(err)
865  return self.async_show_progress_doneasync_show_progress_done(next_step_id="flasher_failed")
866  finally:
867  self.start_taskstart_task = None
868 
869  return self.async_show_progress_doneasync_show_progress_done(next_step_id="flashing_complete")
870 
872  self, user_input: dict[str, Any] | None = None
873  ) -> ConfigFlowResult:
874  """Flasher add-on start failed."""
875  flasher_manager = get_flasher_addon_manager(self.hass)
876  return self.async_abortasync_abort(
877  reason="addon_start_failed",
878  description_placeholders={"addon_name": flasher_manager.addon_name},
879  )
880 
882  self, user_input: dict[str, Any] | None = None
883  ) -> ConfigFlowResult:
884  """Finish flashing and update the config entry."""
885  flasher_manager = get_flasher_addon_manager(self.hass)
886  await flasher_manager.async_uninstall_addon_waiting()
887 
888  # Finish ZHA migration if needed
889  if self._zha_migration_mgr_zha_migration_mgr:
890  try:
891  await self._zha_migration_mgr_zha_migration_mgr.async_finish_migration()
892  except Exception as err:
893  _LOGGER.exception("Unexpected exception during ZHA migration")
894  raise AbortFlow("zha_migration_failed") from err
895 
896  return self.async_create_entryasync_create_entry(title="", data={})
897 
898 
899 async def check_multi_pan_addon(hass: HomeAssistant) -> None:
900  """Check the multiprotocol addon state, and start it if installed but not started.
901 
902  Does nothing if Hass.io is not loaded.
903  Raises on error or if the add-on is installed but not started.
904  """
905  if not is_hassio(hass):
906  return
907 
908  multipan_manager = await get_multiprotocol_addon_manager(hass)
909  try:
910  addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
911  except AddonError as err:
912  _LOGGER.error(err)
913  raise HomeAssistantError from err
914 
915  # Request the addon to start if it's not started
916  # `async_start_addon` returns as soon as the start request has been sent
917  # and does not wait for the addon to be started, so we raise below
918  if addon_info.state == AddonState.NOT_RUNNING:
919  await multipan_manager.async_start_addon()
920 
921  if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING):
922  _LOGGER.debug("Multi pan addon installed and in state %s", addon_info.state)
923  raise HomeAssistantError
924 
925 
926 async def multi_pan_addon_using_device(hass: HomeAssistant, device_path: str) -> bool:
927  """Return True if the multi-PAN addon is using the given device.
928 
929  Returns False if Hass.io is not loaded, the addon is not running or the addon is
930  connected to another device.
931  """
932  if not is_hassio(hass):
933  return False
934 
935  multipan_manager = await get_multiprotocol_addon_manager(hass)
936  addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
937 
938  if addon_info.state != AddonState.RUNNING:
939  return False
940 
941  if addon_info.options["device"] != device_path:
942  return False
943 
944  return True
asyncio.Task async_schedule_install_addon(self, bool catch_error=False)
asyncio.Task async_schedule_start_addon(self, bool catch_error=False)
asyncio.Task|None async_change_channel(self, HomeAssistant hass, int channel, float delay)
None _register_multipan_platform(self, HomeAssistant hass, str integration_domain, MultipanProtocol platform)
ConfigFlowResult async_step_addon_installed_other_device(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_notify_unknown_multipan_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_uninstall_multiprotocol_addon(self, dict[str, Any]|None user_input=None)
None config_entry(self, ConfigEntry value)
str
_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_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)
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
str hostname_from_addon_slug(str addon_slug)
Definition: __init__.py:292
None async_process_integration_platforms(HomeAssistant hass, str platform_name, Callable[[HomeAssistant, str, Any], Awaitable[None]|None] process_platform, bool wait_for_platforms=False)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444