1 """Config flow for ZHA."""
3 from __future__
import annotations
6 from collections.abc
import AsyncIterator
8 from contextlib
import suppress
13 from typing
import Any, Self
15 from bellows.config
import CONF_USE_THREAD
16 import voluptuous
as vol
17 from zha.application.const
import RadioType
18 from zigpy.application
import ControllerApplication
20 from zigpy.config
import (
24 CONF_NWK_BACKUP_ENABLED,
27 from zigpy.exceptions
import NetworkNotFormed
29 from homeassistant
import config_entries
37 DEFAULT_DATABASE_NAME,
40 from .helpers
import get_zha_data
52 RECOMMENDED_RADIOS = (
62 MIGRATION_RETRIES = 100
65 DEVICE_SCHEMA = vol.Schema(
67 vol.Required(
"path"): str,
68 vol.Optional(
"baudrate", default=115200): int,
69 vol.Optional(
"flow_control", default=
None): vol.In(
70 [
"hardware",
"software",
None]
75 HARDWARE_DISCOVERY_SCHEMA = vol.Schema(
77 vol.Required(
"name"): str,
78 vol.Required(
"port"): DEVICE_SCHEMA,
79 vol.Required(
"radio_type"): str,
83 HARDWARE_MIGRATION_SCHEMA = vol.Schema(
85 vol.Required(
"new_discovery_info"): HARDWARE_DISCOVERY_SCHEMA,
86 vol.Required(
"old_discovery_info"): vol.Schema(
88 vol.Exclusive(
"hw",
"discovery"): HARDWARE_DISCOVERY_SCHEMA,
95 _LOGGER = logging.getLogger(__name__)
99 """Radio firmware probing result."""
101 RADIO_TYPE_DETECTED =
"radio_type_detected"
102 WRONG_FIRMWARE_INSTALLED =
"wrong_firmware_installed"
103 PROBING_FAILED =
"probing_failed"
107 backup: zigpy.backups.NetworkBackup,
108 ) -> zigpy.backups.NetworkBackup:
109 """Return a new backup with the flag to allow overwriting the EZSP EUI64."""
110 new_stack_specific = copy.deepcopy(backup.network_info.stack_specific)
111 new_stack_specific.setdefault(
"ezsp", {})[EZSP_OVERWRITE_EUI64] =
True
113 return backup.replace(
114 network_info=backup.network_info.replace(stack_specific=new_stack_specific)
119 backup: zigpy.backups.NetworkBackup,
120 ) -> zigpy.backups.NetworkBackup:
121 """Return a new backup without the flag to allow overwriting the EZSP EUI64."""
122 if "ezsp" not in backup.network_info.stack_specific:
125 new_stack_specific = copy.deepcopy(backup.network_info.stack_specific)
126 new_stack_specific.setdefault(
"ezsp", {}).pop(EZSP_OVERWRITE_EUI64,
None)
128 return backup.replace(
129 network_info=backup.network_info.replace(stack_specific=new_stack_specific)
134 """Helper class with radio related functionality."""
139 """Initialize ZhaRadioManager instance."""
140 self.device_path: str |
None =
None
142 self.
radio_typeradio_type: RadioType |
None =
None
143 self.
current_settingscurrent_settings: zigpy.backups.NetworkBackup |
None =
None
144 self.
backupsbackups: list[zigpy.backups.NetworkBackup] = []
145 self.chosen_backup: zigpy.backups.NetworkBackup |
None =
None
151 """Create an instance from a config entry."""
154 mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
155 mgr.device_settings = config_entry.data[CONF_DEVICE]
156 mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]
160 @contextlib.asynccontextmanager
162 """Connect to the radio with the current config and then clean up."""
166 app_config = config.get(CONF_ZIGPY, {}).copy()
168 database_path = config.get(
170 self.hass.config.path(DEFAULT_DATABASE_NAME),
174 if not await self.hass.async_add_executor_job(os.path.exists, database_path):
177 app_config[CONF_DATABASE] = database_path
179 app_config[CONF_NWK_BACKUP_ENABLED] =
False
180 app_config[CONF_USE_THREAD] =
False
182 app = await self.
radio_typeradio_type.controller.new(
183 app_config, auto_form=
False, start_radio=
False
190 await asyncio.sleep(CONNECT_DELAY_S)
193 self, backup: zigpy.backups.NetworkBackup, **kwargs: Any
195 """Restore the provided network backup, passing through kwargs."""
203 await app.backups.restore_backup(backup, **kwargs)
207 """Parse a radio type name, accounting for past aliases."""
208 if radio_type ==
"efr32":
209 return RadioType.ezsp
211 return RadioType[radio_type]
214 """Probe all radio types on the current port."""
215 assert self.device_path
is not None
217 for radio
in AUTOPROBE_RADIOS:
218 _LOGGER.debug(
"Attempting to probe radio type %s", radio)
220 dev_config =
SCHEMA_DEVICE({CONF_DEVICE_PATH: self.device_path})
221 probe_result = await radio.controller.probe(dev_config)
227 if isinstance(probe_result, dict):
228 dev_config = probe_result
233 repairs.async_delete_blocking_issues(self.hass)
234 return ProbeResult.RADIO_TYPE_DETECTED
237 if await repairs.wrong_silabs_firmware.warn_on_wrong_silabs_firmware(
238 self.hass, self.device_path
240 return ProbeResult.WRONG_FIRMWARE_INSTALLED
242 return ProbeResult.PROBING_FAILED
245 self, *, create_backup: bool =
False
246 ) -> zigpy.backups.NetworkBackup |
None:
247 """Connect to the radio and load its current network settings."""
255 await app.load_network_info()
256 except NetworkNotFormed:
260 network_info=app.state.network_info,
261 node_info=app.state.node_info,
265 backup = await app.backups.create_backup()
268 self.
backupsbackups = app.backups.backups.copy()
269 self.
backupsbackups.sort(reverse=
True, key=
lambda b: b.backup_time)
274 """Form a brand-new network."""
277 await app.form_network()
280 """Reset the current adapter."""
283 await app.reset_network_info()
286 """Prepare restoring backup.
288 Returns True if async_restore_backup_step_2 should be called.
290 assert self.chosen_backup
is not None
292 if self.
radio_typeradio_type != RadioType.ezsp:
302 await self.
restore_backuprestore_backup(temp_backup, create_new=
False)
307 metadata = self.
current_settingscurrent_settings.network_info.metadata[
"ezsp"]
310 self.
current_settingscurrent_settings.node_info.ieee == self.chosen_backup.node_info.ieee
311 or metadata[
"can_rewrite_custom_eui64"]
312 or not metadata[
"can_burn_userdata_custom_eui64"]
323 """Restore backup and optionally overwrite IEEE."""
324 assert self.chosen_backup
is not None
326 backup = self.chosen_backup
337 """Helper class for automatic migration when upgrading the firmware of a radio.
339 This class is currently only intended to be used when changing the firmware on the
340 radio used in the Home Assistant SkyConnect USB stick and the Home Assistant Yellow
341 from Zigbee only firmware to firmware supporting both Zigbee and Thread.
347 """Initialize MigrationHelper instance."""
354 """Initiate ZHA migration.
356 The passed data should contain:
357 - Discovery data identifying the device being firmware updated
358 - Discovery data for connecting to the device after the firmware update is
361 Returns True if async_finish_migration should be called after the firmware
366 name = migration_data[
"new_discovery_info"][
"name"]
367 new_radio_type = ZhaRadioManager.parse_radio_type(
368 migration_data[
"new_discovery_info"][
"radio_type"]
372 migration_data[
"new_discovery_info"][
"port"]
375 if "hw" in migration_data[
"old_discovery_info"]:
376 old_device_path = migration_data[
"old_discovery_info"][
"hw"][
"port"][
"path"]
378 device = migration_data[
"old_discovery_info"][
"usb"].device
379 old_device_path = await self.
_hass_hass.async_add_executor_job(
380 usb.get_serial_by_id, device
383 if self.
_config_entry_config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] != old_device_path:
389 await self.
_hass_hass.config_entries.async_unload(self.
_config_entry_config_entry.entry_id)
394 old_radio_mgr.hass = self.
_hass_hass
395 old_radio_mgr.device_path = config_entry_data[CONF_DEVICE][CONF_DEVICE_PATH]
396 old_radio_mgr.device_settings = config_entry_data[CONF_DEVICE]
397 old_radio_mgr.radio_type = RadioType[config_entry_data[CONF_RADIO_TYPE]]
399 for retry
in range(BACKUP_RETRIES):
401 backup = await old_radio_mgr.async_load_network_settings(
405 except OSError
as err:
406 if retry >= BACKUP_RETRIES - 1:
410 "Failed to create backup %r, retrying in %s seconds",
415 await asyncio.sleep(RETRY_DELAY_S)
418 self.
_radio_mgr_radio_mgr.chosen_backup = backup
419 self.
_radio_mgr_radio_mgr.radio_type = new_radio_type
420 self.
_radio_mgr_radio_mgr.device_path = new_device_settings[CONF_DEVICE_PATH]
421 self.
_radio_mgr_radio_mgr.device_settings = new_device_settings
422 device_settings = self.
_radio_mgr_radio_mgr.device_settings.copy()
425 self.
_hass_hass.config_entries.async_update_entry(
428 CONF_DEVICE: device_settings,
429 CONF_RADIO_TYPE: self.
_radio_mgr_radio_mgr.radio_type.name,
437 """Finish ZHA migration.
439 Throws an exception if the migration did not succeed.
442 for retry
in range(MIGRATION_RETRIES):
444 if await self.
_radio_mgr_radio_mgr.async_restore_backup_step_1():
445 await self.
_radio_mgr_radio_mgr.async_restore_backup_step_2(
True)
448 except OSError
as err:
449 if retry >= MIGRATION_RETRIES - 1:
453 "Failed to restore backup %r, retrying in %s seconds",
458 await asyncio.sleep(RETRY_DELAY_S)
460 _LOGGER.debug(
"Restored backup after %s retries", retry)
465 await self.
_hass_hass.config_entries.async_setup(self.
_config_entry_config_entry.entry_id)
None __init__(self, HomeAssistant hass, config_entries.ConfigEntry config_entry)
bool async_initiate_migration(self, dict[str, Any] data)
None async_finish_migration(self)
None async_form_network(self)
AsyncIterator[ControllerApplication] connect_zigpy_app(self)
ProbeResult detect_radio_type(self)
None restore_backup(self, zigpy.backups.NetworkBackup backup, **Any kwargs)
bool async_restore_backup_step_1(self)
None async_restore_backup_step_2(self, bool overwrite_ieee)
None async_reset_adapter(self)
Self from_config_entry(cls, HomeAssistant hass, config_entries.ConfigEntry config_entry)
RadioType parse_radio_type(str radio_type)
zigpy.backups.NetworkBackup|None async_load_network_settings(self, *bool create_backup=False)
HAZHAData get_zha_data(HomeAssistant hass)
zigpy.backups.NetworkBackup _prevent_overwrite_ezsp_ieee(zigpy.backups.NetworkBackup backup)
HARDWARE_MIGRATION_SCHEMA
zigpy.backups.NetworkBackup _allow_overwrite_ezsp_ieee(zigpy.backups.NetworkBackup backup)