diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 64590a69a77..2e24ddc070d 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.components.hassio import ( ) from homeassistant.config_entries import ( SOURCE_USB, + ConfigEntry, ConfigEntryState, ConfigFlow, ConfigFlowResult, @@ -77,9 +78,6 @@ CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" SERVER_VERSION_TIMEOUT = 10 -OPTIONS_INTENT_MIGRATE = "intent_migrate" -OPTIONS_INTENT_RECONFIGURE = "intent_reconfigure" - ADDON_LOG_LEVELS = { "error": "Error", "warn": "Warn", @@ -203,7 +201,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.backup_data: bytes | None = None self.backup_filepath: str | None = None self.use_addon = False - self._reconfiguring = False + self._reconfigure_config_entry: ConfigEntry | None = None self._usb_discovery = False async def async_step_install_addon( @@ -266,8 +264,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Add-on start failed.""" - if self._reconfiguring: - return await self.async_step_start_failed_reconfigure() + if self._reconfigure_config_entry: + return await self.async_revert_addon_config(reason="addon_start_failed") return self.async_abort(reason="addon_start_failed") async def _async_start_addon(self) -> None: @@ -305,7 +303,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Ask for config for Z-Wave JS add-on.""" - if self._reconfiguring: + if self._reconfigure_config_entry: return await self.async_step_configure_addon_reconfigure(user_input) return await self.async_step_configure_addon_user(user_input) @@ -317,7 +315,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): Get add-on discovery info and server version info. Set unique id and abort if already configured. """ - if self._reconfiguring: + if self._reconfigure_config_entry: return await self.async_step_finish_addon_setup_reconfigure(user_input) return await self.async_step_finish_addon_setup_user(user_input) @@ -332,11 +330,25 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): return addon_info - async def _async_set_addon_config(self, config: dict) -> None: + async def _async_set_addon_config(self, config_updates: dict) -> None: """Set Z-Wave JS add-on config.""" + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options + + new_addon_config = addon_config | config_updates + + if new_addon_config == addon_config: + return + + if addon_info.state == AddonState.RUNNING: + self.restart_addon = True + # Copy the add-on config to keep the objects separate. + self.original_addon_config = dict(addon_config) + # Remove legacy network_key + new_addon_config.pop(CONF_ADDON_NETWORK_KEY, None) addon_manager: AddonManager = get_addon_manager(self.hass) try: - await addon_manager.async_set_addon_options(config) + await addon_manager.async_set_addon_options(new_addon_config) except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_set_config_failed") from err @@ -370,12 +382,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm if we are migrating adapters or just re-configuring.""" - self._reconfiguring = True + self._reconfigure_config_entry = self._get_reconfigure_entry() return self.async_show_menu( step_id="reconfigure", menu_options=[ - OPTIONS_INTENT_RECONFIGURE, - OPTIONS_INTENT_MIGRATE, + "intent_reconfigure", + "intent_migrate", ], ) @@ -432,7 +444,10 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id( f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" ) - self._abort_if_unique_id_configured() + # We don't need to check if the unique_id is already configured + # since we will update the unique_id before finishing the flow. + # The unique_id set above is just a temporary value to avoid + # duplicate discovery flows. dev_path = discovery_info.device self.usb_path = dev_path if manufacturer == "Nabu Casa" and description == "ZWA-2 - Nabu Casa ZWA-2": @@ -598,8 +613,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if not self._usb_discovery: self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = { - **addon_config, + addon_config_updates = { CONF_ADDON_DEVICE: self.usb_path, CONF_ADDON_S0_LEGACY_KEY: self.s0_legacy_key, CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, @@ -609,8 +623,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDON_LR_S2_AUTHENTICATED_KEY: self.lr_s2_authenticated_key, } - if new_addon_config != addon_config: - await self._async_set_addon_config(new_addon_config) + await self._async_set_addon_config(addon_config_updates) return await self.async_step_start_addon() @@ -730,11 +743,17 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) @callback - def _async_update_entry(self, data: dict[str, Any]) -> None: + def _async_update_entry( + self, updates: dict[str, Any], *, schedule_reload: bool = True + ) -> None: """Update the config entry with new data.""" + config_entry = self._reconfigure_config_entry + assert config_entry is not None self.hass.config_entries.async_update_entry( - self._get_reconfigure_entry(), data=data + config_entry, data=config_entry.data | updates ) + if schedule_reload: + self.hass.config_entries.async_schedule_reload(config_entry.entry_id) async def async_step_intent_reconfigure( self, user_input: dict[str, Any] | None = None @@ -749,7 +768,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm the user wants to reset their current controller.""" - if not self._get_reconfigure_entry().data.get(CONF_USE_ADDON): + config_entry = self._reconfigure_config_entry + assert config_entry is not None + if not self._usb_discovery and not config_entry.data.get(CONF_USE_ADDON): return self.async_abort(reason="addon_required") if user_input is not None: @@ -834,7 +855,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a manual configuration.""" - config_entry = self._get_reconfigure_entry() + config_entry = self._reconfigure_config_entry + assert config_entry is not None if user_input is None: return self.async_show_form( step_id="manual_reconfigure", @@ -858,14 +880,12 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): # if the controller is reconfigured in a manual step. self._async_update_entry( { - **config_entry.data, **user_input, CONF_USE_ADDON: False, CONF_INTEGRATION_CREATED_ADDON: False, } ) - self.hass.config_entries.async_schedule_reload(config_entry.entry_id) return self.async_abort(reason="reconfigure_successful") return self.async_show_form( @@ -878,7 +898,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle logic when on Supervisor host.""" - config_entry = self._get_reconfigure_entry() + config_entry = self._reconfigure_config_entry + assert config_entry is not None if user_input is None: return self.async_show_form( step_id="on_supervisor_reconfigure", @@ -914,7 +935,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Ask for config for Z-Wave JS add-on.""" - config_entry = self._get_reconfigure_entry() addon_info = await self._async_get_addon_info() addon_config = addon_info.options @@ -927,8 +947,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self.lr_s2_authenticated_key = user_input[CONF_LR_S2_AUTHENTICATED_KEY] self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = { - **addon_config, + addon_config_updates = { CONF_ADDON_DEVICE: self.usb_path, CONF_ADDON_S0_LEGACY_KEY: self.s0_legacy_key, CONF_ADDON_S2_ACCESS_CONTROL_KEY: self.s2_access_control_key, @@ -942,19 +961,14 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ), } - if new_addon_config != addon_config: - if addon_info.state == AddonState.RUNNING: - self.restart_addon = True - # Copy the add-on config to keep the objects separate. - self.original_addon_config = dict(addon_config) - # Remove legacy network_key - new_addon_config.pop(CONF_ADDON_NETWORK_KEY, None) - await self._async_set_addon_config(new_addon_config) + await self._async_set_addon_config(addon_config_updates) if addon_info.state == AddonState.RUNNING and not self.restart_addon: return await self.async_step_finish_addon_setup_reconfigure() - if config_entry.data.get(CONF_USE_ADDON): + if ( + config_entry := self._reconfigure_config_entry + ) and config_entry.data.get(CONF_USE_ADDON): # Disconnect integration before restarting add-on. await self.hass.config_entries.async_unload(config_entry.entry_id) @@ -1021,18 +1035,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Choose a serial port.""" if user_input is not None: - addon_info = await self._async_get_addon_info() - addon_config = addon_info.options self.usb_path = user_input[CONF_USB_PATH] - new_addon_config = { - **addon_config, - CONF_ADDON_DEVICE: self.usb_path, - } - if addon_info.state == AddonState.RUNNING: - self.restart_addon = True - # Copy the add-on config to keep the objects separate. - self.original_addon_config = dict(addon_config) - await self._async_set_addon_config(new_addon_config) + await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path}) return await self.async_step_start_addon() try: @@ -1050,12 +1054,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): step_id="choose_serial_port", data_schema=data_schema ) - async def async_step_start_failed_reconfigure( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: - """Add-on start failed.""" - return await self.async_revert_addon_config(reason="addon_start_failed") - async def async_step_backup_failed( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -1082,7 +1080,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): Get add-on discovery info and server version info. Check for same unique id and abort if not the same unique id. """ - config_entry = self._get_reconfigure_entry() + config_entry = self._reconfigure_config_entry + assert config_entry is not None if self.revert_reason: self.original_addon_config = None reason = self.revert_reason @@ -1108,9 +1107,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): self._async_update_entry( { - **config_entry.data, - # this will only be different in a migration flow - "unique_id": str(self.version_info.home_id), CONF_URL: self.ws_address, CONF_USB_PATH: self.usb_path, CONF_S0_LEGACY_KEY: self.s0_legacy_key, @@ -1126,8 +1122,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): if self.backup_data: return await self.async_step_restore_nvm() - # Always reload entry since we may have disconnected the client. - self.hass.config_entries.async_schedule_reload(config_entry.entry_id) return self.async_abort(reason="reconfigure_successful") async def async_revert_addon_config(self, reason: str) -> ConfigFlowResult: @@ -1143,9 +1137,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): ) if self.revert_reason or not self.original_addon_config: - self.hass.config_entries.async_schedule_reload( - self._get_reconfigure_entry().entry_id - ) + config_entry = self._reconfigure_config_entry + assert config_entry is not None + self.hass.config_entries.async_schedule_reload(config_entry.entry_id) return self.async_abort(reason=reason) self.revert_reason = reason @@ -1189,11 +1183,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_restore_network_backup(self) -> None: """Restore the backup.""" assert self.backup_data is not None + config_entry = self._reconfigure_config_entry + assert config_entry is not None # Reload the config entry to reconnect the client after the addon restart - await self.hass.config_entries.async_reload( - self._get_reconfigure_entry().entry_id - ) + await self.hass.config_entries.async_reload(config_entry.entry_id) @callback def forward_progress(event: dict) -> None: @@ -1222,7 +1216,8 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN): def _get_driver(self) -> Driver: """Get the driver from the config entry.""" - config_entry = self._get_reconfigure_entry() + config_entry = self._reconfigure_config_entry + assert config_entry is not None if config_entry.state != ConfigEntryState.LOADED: raise AbortFlow("Configuration entry is not loaded") client: Client = config_entry.runtime_data[DATA_CLIENT]