1 """Manage the Silicon Labs Multiprotocol add-on."""
3 from __future__
import annotations
5 from abc
import ABC, abstractmethod
9 from typing
import Any, Protocol
11 import voluptuous
as vol
19 hostname_from_addon_slug,
32 async_process_integration_platforms,
42 from .const
import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
44 _LOGGER = logging.getLogger(__name__)
46 DATA_MULTIPROTOCOL_ADDON_MANAGER =
"silabs_multiprotocol_addon_manager"
47 DATA_FLASHER_ADDON_MANAGER =
"silabs_flasher"
49 ADDON_STATE_POLL_INTERVAL = 3
50 ADDON_INFO_POLL_TIMEOUT = 15 * 60
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"
58 DEFAULT_CHANNEL_CHANGE_DELAY = 5 * 60
60 STORAGE_KEY =
"homeassistant_hardware.silabs"
61 STORAGE_VERSION_MAJOR = 1
62 STORAGE_VERSION_MINOR = 1
66 @singleton(DATA_MULTIPROTOCOL_ADDON_MANAGER)
69 ) -> MultiprotocolAddonManager:
70 """Get the add-on manager."""
72 await manager.async_setup()
77 """Addon manager which supports waiting operations for managing an addon."""
80 """Poll an addon's info until it is in a specific state."""
81 async
with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
88 _LOGGER.debug(
"Waiting for addon to be in state %s: %s", states, info)
90 if info
is not None and info.state
in states:
93 await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
95 async
def async_start_addon_waiting(self) -> None:
96 """Start an add-on."""
100 async
def async_install_addon_waiting(self) -> None:
101 """Install an add-on."""
105 AddonState.NOT_RUNNING,
108 async
def async_uninstall_addon_waiting(self) -> None:
109 """Uninstall an add-on."""
116 if info
is not None and info.state == AddonState.NOT_INSTALLED:
124 """Silicon Labs Multiprotocol add-on manager."""
127 """Initialize the manager."""
131 "Silicon Labs Multiprotocol",
132 SILABS_MULTIPROTOCOL_ADDON_SLUG,
134 self.
_channel_channel: int |
None =
None
135 self._platforms: dict[str, MultipanProtocol] = {}
136 self._store: Store[dict[str, Any]] =
Store(
138 STORAGE_VERSION_MAJOR,
141 minor_version=STORAGE_VERSION_MINOR,
145 """Set up the manager."""
148 "silabs_multiprotocol",
150 wait_for_platforms=
True,
155 self, hass: HomeAssistant, integration_domain: str, platform: MultipanProtocol
157 """Register a multipan platform."""
158 self._platforms[integration_domain] = platform
160 channel = await platform.async_get_channel(hass)
161 using_multipan = await platform.async_using_multipan(hass)
164 "Registering new multipan platform '%s', using multipan: %s, channel: %s",
170 if self.
_channel_channel
is not None or not using_multipan:
177 "Setting multipan channel to %s (source: '%s')",
184 self, channel: int, delay: float
185 ) -> list[asyncio.Task]:
186 """Change the channel and notify platforms."""
191 for platform
in self._platforms.values():
192 if not await platform.async_using_multipan(self.
_hass_hass):
194 task = await platform.async_change_channel(self.
_hass_hass, channel, delay)
202 """Return a list of platforms using the multipan radio."""
203 active_platforms: list[str] = []
205 for integration_domain, platform
in self._platforms.items():
206 if not await platform.async_using_multipan(self.
_hass_hass):
208 active_platforms.append(integration_domain)
210 return active_platforms
214 """Get the channel."""
219 """Set the channel without notifying platforms.
221 This must only be called when first initializing the manager.
227 """Load the store."""
231 self.
_channel_channel = data[
"channel"]
235 """Schedule saving the store."""
240 """Return data to store in a file."""
241 data: dict[str, Any] = {}
242 data[
"channel"] = self.
_channel_channel
247 """Define the format of multipan platforms."""
250 self, hass: HomeAssistant, channel: int, delay: float
251 ) -> asyncio.Task |
None:
252 """Set the channel to be used.
254 Does nothing if not configured or the multiprotocol add-on is not used.
258 """Return the channel.
260 Returns None if not configured or the multiprotocol add-on is not used.
264 """Return if the multiprotocol device is used.
266 Returns False if not configured.
270 @singleton(DATA_FLASHER_ADDON_MANAGER)
273 """Get the flasher add-on manager."""
277 "Silicon Labs Flasher",
278 SILABS_FLASHER_ADDON_SLUG,
282 @dataclasses.dataclass
284 """Serial port settings."""
292 """Return the zigbee socket.
294 Raises AddonError on error
297 return f
"socket://{hostname}:9999"
301 """Return if the URL points at the Multiprotocol add-on."""
302 parsed = yarl.URL(url)
304 return parsed.host == hostname
308 """Handle an options flow for the Silicon Labs Multiprotocol add-on."""
310 def __init__(self, config_entry: ConfigEntry) ->
None:
311 """Set up the options flow."""
314 ZhaMultiPANMigrationHelper,
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
322 self.revert_reason: str |
None =
None
326 """Return the radio serial port settings."""
330 """Return ZHA discovery data when multiprotocol FW is not used.
332 Passed to ZHA do determine if the ZHA config entry is connected to the radio
338 """Return the name of the hardware."""
342 """Return the ZHA name."""
346 """Return the correct flow manager."""
347 return self.hass.config_entries.options
350 """Return and cache Silicon Labs Multiprotocol add-on info."""
352 addon_info: AddonInfo = await addon_manager.async_get_addon_info()
353 except AddonError
as err:
357 description_placeholders={
"addon_name": addon_manager.addon_name},
363 self, config: dict, addon_manager: AddonManager
365 """Set Silicon Labs Multiprotocol add-on config."""
367 await addon_manager.async_set_addon_options(config)
368 except AddonError
as err:
370 raise AbortFlow(
"addon_set_config_failed")
from err
373 self, user_input: dict[str, Any] |
None =
None
374 ) -> ConfigFlowResult:
375 """Manage the options."""
377 return self.
async_abortasync_abort(reason=
"not_hassio")
382 self, user_input: dict[str, Any] |
None =
None
383 ) -> ConfigFlowResult:
384 """Handle logic when on Supervisor host."""
388 if addon_info.state == AddonState.NOT_INSTALLED:
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:
398 step_id=
"addon_not_installed",
399 data_schema=vol.Schema(
400 {vol.Required(CONF_ENABLE_MULTI_PAN, default=
False): bool}
402 description_placeholders={
"hardware_name": self.
_hardware_name_hardware_name()},
404 if not user_input[CONF_ENABLE_MULTI_PAN]:
410 self, user_input: dict[str, Any] |
None =
None
411 ) -> ConfigFlowResult:
412 """Install Silicon Labs Multiprotocol add-on."""
417 multipan_manager.async_install_addon_waiting(),
418 "SiLabs Multiprotocol addon install",
424 step_id=
"install_addon",
425 progress_action=
"install_addon",
426 description_placeholders={
"addon_name": multipan_manager.addon_name},
432 except AddonError
as err:
441 self, user_input: dict[str, Any] |
None =
None
442 ) -> ConfigFlowResult:
443 """Add-on installation failed."""
446 reason=
"addon_install_failed",
447 description_placeholders={
"addon_name": multipan_manager.addon_name},
451 self, user_input: dict[str, Any] |
None =
None
452 ) -> ConfigFlowResult:
453 """Configure the Silicon Labs Multiprotocol add-on."""
459 ZhaMultiPANMigrationHelper,
464 async_get_channel
as async_get_zha_channel,
470 addon_config = addon_info.options
475 CONF_ADDON_AUTOFLASH_FW:
True,
476 **dataclasses.asdict(serial_port_settings),
479 multipan_channel = DEFAULT_CHANNEL
482 zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
485 zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0])
487 "new_discovery_info": {
492 "radio_type":
"ezsp",
496 _LOGGER.debug(
"Starting ZHA migration with: %s", migration_data)
498 if await zha_migration_mgr.async_initiate_migration(migration_data):
500 except Exception
as err:
501 _LOGGER.exception(
"Unexpected exception during ZHA migration")
502 raise AbortFlow(
"zha_migration_failed")
from err
504 if (zha_channel := await async_get_zha_channel(self.hass))
is not None:
505 multipan_channel = zha_channel
509 multipan_manager.async_set_channel(multipan_channel)
511 if new_addon_config != addon_config:
514 _LOGGER.debug(
"Reconfiguring addon with %s", new_addon_config)
520 self, user_input: dict[str, Any] |
None =
None
521 ) -> ConfigFlowResult:
522 """Start Silicon Labs Multiprotocol add-on."""
527 multipan_manager.async_start_addon_waiting(), eager_start=
False
532 step_id=
"start_addon",
533 progress_action=
"start_addon",
534 description_placeholders={
"addon_name": multipan_manager.addon_name},
540 except (AddonError, AbortFlow)
as err:
549 self, user_input: dict[str, Any] |
None =
None
550 ) -> ConfigFlowResult:
551 """Add-on start failed."""
554 reason=
"addon_start_failed",
555 description_placeholders={
"addon_name": multipan_manager.addon_name},
559 self, user_input: dict[str, Any] |
None =
None
560 ) -> ConfigFlowResult:
561 """Prepare info needed to complete the config entry update."""
563 self.hass.async_create_task(
572 except Exception
as err:
573 _LOGGER.exception(
"Unexpected exception during ZHA migration")
574 raise AbortFlow(
"zha_migration_failed")
from err
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")
587 self, user_input: dict[str, Any] |
None =
None
588 ) -> ConfigFlowResult:
589 """Handle logic when the addon is already installed."""
594 if addon_info.options.get(CONF_ADDON_DEVICE) != serial_device:
599 self, user_input: dict[str, Any] |
None =
None
600 ) -> ConfigFlowResult:
601 """Show menu options for the addon."""
603 step_id=
"addon_menu",
611 self, user_input: dict[str, Any] |
None =
None
612 ) -> ConfigFlowResult:
613 """Reconfigure the addon."""
615 active_platforms = await multipan_manager.async_active_platforms()
616 if set(active_platforms) != {
"otbr",
"zha"}:
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:
626 step_id=
"notify_unknown_multipan_user",
631 self, user_input: dict[str, Any] |
None =
None
632 ) -> ConfigFlowResult:
633 """Change the channel."""
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(
644 description={
"suggested_value":
str(suggested_channel)},
647 options=channels, mode=SelectSelectorMode.DROPDOWN
653 step_id=
"change_channel", data_schema=data_schema
657 await multipan_manager.async_change_channel(
658 int(user_input[
"channel"]), DEFAULT_CHANNEL_CHANGE_DELAY
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:
668 step_id=
"notify_channel_change",
669 description_placeholders={
670 "delay_minutes":
str(DEFAULT_CHANNEL_CHANGE_DELAY // 60)
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:
681 step_id=
"uninstall_addon",
682 data_schema=vol.Schema(
683 {vol.Required(CONF_DISABLE_MULTI_PAN, default=
False): bool}
685 description_placeholders={
"hardware_name": self.
_hardware_name_hardware_name()},
687 if not user_input[CONF_DISABLE_MULTI_PAN]:
693 self, user_input: dict[str, Any] |
None =
None
694 ) -> ConfigFlowResult:
695 """Install the flasher addon, if necessary."""
700 if addon_info.state == AddonState.NOT_INSTALLED:
703 if addon_info.state == AddonState.NOT_RUNNING:
708 reason=
"addon_already_running",
709 description_placeholders={
"addon_name": flasher_manager.addon_name},
713 self, user_input: dict[str, Any] |
None =
None
714 ) -> ConfigFlowResult:
715 """Show progress dialog for installing flasher addon."""
719 _LOGGER.debug(
"Flasher addon state: %s", addon_info)
722 self.
install_taskinstall_task = self.hass.async_create_task(
723 flasher_manager.async_install_addon_waiting(),
724 "SiLabs Flasher addon install",
730 step_id=
"install_flasher_addon",
731 progress_action=
"install_addon",
732 description_placeholders={
"addon_name": flasher_manager.addon_name},
738 except AddonError
as err:
747 self, user_input: dict[str, Any] |
None =
None
748 ) -> ConfigFlowResult:
749 """Perform initial backup and reconfigure ZHA."""
755 ZhaMultiPANMigrationHelper,
758 zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
761 _LOGGER.debug(
"Using new ZHA settings: %s", new_settings)
764 zha_migration_mgr = ZhaMultiPANMigrationHelper(self.hass, zha_entries[0])
766 "new_discovery_info": {
769 "path": new_settings.device,
770 "baudrate":
int(new_settings.baudrate),
772 "hardware" if new_settings.flow_control
else None
775 "radio_type":
"ezsp",
777 "old_discovery_info": {
781 "radio_type":
"ezsp",
785 _LOGGER.debug(
"Starting ZHA migration with: %s", migration_data)
787 if await zha_migration_mgr.async_initiate_migration(migration_data):
789 except Exception
as err:
790 _LOGGER.exception(
"Unexpected exception during ZHA migration")
791 raise AbortFlow(
"zha_migration_failed")
from err
796 **addon_info.options,
797 "device": new_settings.device,
798 "flow_control": new_settings.flow_control,
801 _LOGGER.debug(
"Reconfiguring flasher addon with %s", new_addon_config)
807 self, user_input: dict[str, Any] |
None =
None
808 ) -> ConfigFlowResult:
809 """Uninstall Silicon Labs Multiprotocol add-on."""
814 multipan_manager.async_uninstall_addon_waiting(),
815 "SiLabs Multiprotocol addon uninstall",
821 step_id=
"uninstall_multiprotocol_addon",
822 progress_action=
"uninstall_multiprotocol_addon",
823 description_placeholders={
"addon_name": multipan_manager.addon_name},
835 self, user_input: dict[str, Any] |
None =
None
836 ) -> ConfigFlowResult:
837 """Start Silicon Labs Flasher add-on."""
842 async
def start_and_wait_until_done() -> None:
843 await flasher_manager.async_start_addon_waiting()
845 await flasher_manager.async_wait_until_addon_state(
846 AddonState.NOT_RUNNING
849 self.
start_taskstart_task = self.hass.async_create_task(
850 start_and_wait_until_done(), eager_start=
False
855 step_id=
"start_flasher_addon",
856 progress_action=
"start_flasher_addon",
857 description_placeholders={
"addon_name": flasher_manager.addon_name},
863 except (AddonError, AbortFlow)
as err:
872 self, user_input: dict[str, Any] |
None =
None
873 ) -> ConfigFlowResult:
874 """Flasher add-on start failed."""
877 reason=
"addon_start_failed",
878 description_placeholders={
"addon_name": flasher_manager.addon_name},
882 self, user_input: dict[str, Any] |
None =
None
883 ) -> ConfigFlowResult:
884 """Finish flashing and update the config entry."""
886 await flasher_manager.async_uninstall_addon_waiting()
892 except Exception
as err:
893 _LOGGER.exception(
"Unexpected exception during ZHA migration")
894 raise AbortFlow(
"zha_migration_failed")
from err
900 """Check the multiprotocol addon state, and start it if installed but not started.
902 Does nothing if Hass.io is not loaded.
903 Raises on error or if the add-on is installed but not started.
910 addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
911 except AddonError
as err:
913 raise HomeAssistantError
from err
918 if addon_info.state == AddonState.NOT_RUNNING:
919 await multipan_manager.async_start_addon()
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
927 """Return True if the multi-PAN addon is using the given device.
929 Returns False if Hass.io is not loaded, the addon is not running or the addon is
930 connected to another device.
936 addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
938 if addon_info.state != AddonState.RUNNING:
941 if addon_info.options[
"device"] != device_path:
None async_uninstall_addon(self)
AddonInfo async_get_addon_info(self)
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)
int|None async_get_channel(self, HomeAssistant hass)
bool async_using_multipan(self, HomeAssistant hass)
None _register_multipan_platform(self, HomeAssistant hass, str integration_domain, MultipanProtocol platform)
None async_schedule_save(self)
None async_set_channel(self, int channel)
None __init__(self, HomeAssistant hass)
list[str] async_active_platforms(self)
dict[str, list[dict[str, str|None]]] _data_to_save(self)
int|None async_get_channel(self)
list[asyncio.Task] async_change_channel(self, int channel, float delay)
ConfigFlowResult async_step_addon_not_installed(self, dict[str, Any]|None user_input=None)
OptionsFlowManager flow_manager(self)
ConfigFlowResult async_step_install_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_start_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_start_failed(self, dict[str, Any]|None user_input=None)
None __init__(self, ConfigEntry config_entry)
ConfigFlowResult async_step_install_addon(self, dict[str, Any]|None user_input=None)
SerialPortSettings _async_serial_port_settings(self)
AddonInfo _async_get_addon_info(self, AddonManager addon_manager)
ConfigFlowResult async_step_firmware_revert(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_configure_flasher_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_start_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_notify_channel_change(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_flashing_complete(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_flasher_failed(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_configure_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_addon_menu(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_change_channel(self, dict[str, Any]|None user_input=None)
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_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_on_supervisor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_addon_installed(self, dict[str, Any]|None user_input=None)
None _async_set_addon_config(self, dict config, AddonManager addon_manager)
ConfigFlowResult async_step_uninstall_multiprotocol_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_uninstall_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_finish_addon_setup(self, dict[str, Any]|None user_input=None)
dict[str, Any] _async_zha_physical_discovery(self)
ConfigFlowResult async_step_install_failed(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reconfigure_addon(self, dict[str, Any]|None user_input=None)
None async_wait_until_addon_state(self, *AddonState states)
ConfigEntry config_entry(self)
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_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)
str hostname_from_addon_slug(str addon_slug)
MultiprotocolAddonManager get_multiprotocol_addon_manager(HomeAssistant hass)
bool multi_pan_addon_using_device(HomeAssistant hass, str device_path)
WaitingAddonManager get_flasher_addon_manager(HomeAssistant hass)
None check_multi_pan_addon(HomeAssistant hass)
bool is_multiprotocol_url(str url)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)