Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion homeassistant/components/zha/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,10 +473,62 @@ async def async_step_maybe_reset_old_radio(
temp_radio_mgr.device_settings = config_entry.data[CONF_DEVICE]
temp_radio_mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]

await temp_radio_mgr.async_reset_adapter()
try:
await temp_radio_mgr.async_reset_adapter()
except HomeAssistantError:
# Old adapter not found or cannot connect, show prompt to plug back in
return await self.async_step_plug_in_old_radio()

return await self.async_step_maybe_confirm_ezsp_restore()

async def async_step_plug_in_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Prompt user to plug in the old radio if connection fails."""
config_entries = self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False
)

# config entry should always exist here, skip if not
if not config_entries:
return await self.async_step_maybe_confirm_ezsp_restore()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one line missing test coverage. Can write a test for this, but we should never be able to hit this.

We could assert that config_entries is not empty, or we could also save the "old adapter device path" to some instance variable in async_step_maybe_reset_old_radio.
Passing it as an additional argument also works, but it's not a pattern I've seen used for any config flows really.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You conceivably could hit this by deleting ZHA halfway through the flow but I agree, this isn't something we should care about. There are many places in Core that break if you try hard enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the test added I added in bdf3a2c (#155537) won't actually help to cover this, since the plug_in_old_radio won't happen again. The retry_old_radio step goes to maybe_reset_old_radio and that step itself will skip to maybe_confirm_ezsp_restore, as there are no other config entries anymore.

For this line to be hit, the config entry needs to be removed at precisely the exact moment we're trying to reset the adapter. I'll try something different..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bcb573c (#155537) should cover this now and work completely reliable, even if this is almost impossible for a user to do: remove both the config entry and unplug the adapter whilst we try to reset it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative to this would be saving or passing the old adapter's path from async_step_maybe_reset_old_radio. Then, we wouldn't need to get config entries in the new step displaying the old adapter's path, so we could avoid the potential step skip if the entry is removed in time, thus avoiding the whole tests for it as well.

But now, we have the test and everything functions as expected if the user is able to hit this 😄


config_entry = config_entries[0]
old_device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]

return self.async_show_menu(
step_id="plug_in_old_radio",
menu_options=["retry_old_radio", "skip_reset_old_radio"],
description_placeholders={"device_path": old_device_path},
)

async def async_step_retry_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Retry connecting to the old radio."""
return await self.async_step_maybe_reset_old_radio()

async def async_step_skip_reset_old_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Skip resetting the old radio and continue with migration."""
return await self.async_step_maybe_confirm_ezsp_restore()

async def async_step_plug_in_new_radio(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Prompt user to plug in the new radio if connection fails."""
if user_input is not None:
# User confirmed, retry now
return await self.async_step_maybe_confirm_ezsp_restore()

assert self._radio_mgr.device_path is not None

return self.async_show_form(
step_id="plug_in_new_radio",
description_placeholders={"device_path": self._radio_mgr.device_path},
)

async def async_step_migration_strategy_advanced(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
Expand Down Expand Up @@ -633,6 +685,9 @@ async def async_step_maybe_confirm_ezsp_restore(
# On confirmation, overwrite destructively
try:
await self._radio_mgr.restore_backup(overwrite_ieee=True)
except HomeAssistantError:
# User unplugged the new adapter, allow retry
return await self.async_step_plug_in_new_radio()
except CannotWriteNetworkSettings as exc:
return self.async_abort(
reason="cannot_restore_backup",
Expand All @@ -650,6 +705,9 @@ async def async_step_maybe_confirm_ezsp_restore(
except DestructiveWriteNetworkSettings:
# Restore cannot happen automatically, we need to ask for permission
pass
except HomeAssistantError:
# User unplugged the new adapter, allow retry
return await self.async_step_plug_in_new_radio()
except CannotWriteNetworkSettings as exc:
return self.async_abort(
reason="cannot_restore_backup",
Expand Down
32 changes: 32 additions & 0 deletions homeassistant/components/zha/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@
"description": "A backup was created earlier and your old adapter is being reset as part of the migration.",
"title": "Resetting old adapter"
},
"plug_in_new_radio": {
"description": "Your new adapter at `{device_path}` was not found.\nPlease plug it in and click Submit to continue.",
"title": "New adapter not found"
},
"plug_in_old_radio": {
"description": "Your old adapter at `{device_path}` was not found. You can retry after plugging it back in, or skip resetting the old adapter.\n\nIf you skip resetting the old adapter, please make sure it is permanently unavailable, as plugging it back in later will cause network issues.",
"menu_option_descriptions": {
"retry_old_radio": "Retry connecting to the old adapter to reset it as part of the migration.",
"skip_reset_old_radio": "Skip resetting the old adapter and continue with the migration."
},
"menu_options": {
"retry_old_radio": "Retry",
"skip_reset_old_radio": "Skip reset"
},
"title": "Old adapter not found"
},
"upload_manual_backup": {
"data": {
"uploaded_backup_file": "Upload a file"
Expand Down Expand Up @@ -1945,6 +1961,22 @@
"description": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::description%]",
"title": "[%key:component::zha::config::step::maybe_confirm_ezsp_restore::title%]"
},
"plug_in_new_radio": {
"description": "[%key:component::zha::config::step::plug_in_new_radio::description%]",
"title": "[%key:component::zha::config::step::plug_in_new_radio::title%]"
},
"plug_in_old_radio": {
"description": "[%key:component::zha::config::step::plug_in_old_radio::description%]",
"menu_option_descriptions": {
"retry_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_option_descriptions::retry_old_radio%]",
"skip_reset_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_option_descriptions::skip_reset_old_radio%]"
},
"menu_options": {
"retry_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_options::retry_old_radio%]",
"skip_reset_old_radio": "[%key:component::zha::config::step::plug_in_old_radio::menu_options::skip_reset_old_radio%]"
},
"title": "[%key:component::zha::config::step::plug_in_old_radio::title%]"
},
"prompt_migrate_or_reconfigure": {
"description": "Are you migrating to a new adapter or re-configuring the current adapter?",
"menu_option_descriptions": {
Expand Down
Loading
Loading