Add reconfigure flow in Android TV Remote (#148044)

This commit is contained in:
tronikos 2025-07-03 13:00:07 -07:00 committed by GitHub
parent e5f7421703
commit b410b414ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 136 additions and 10 deletions

View File

@ -16,6 +16,7 @@ import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
@ -40,12 +41,6 @@ APPS_NEW_ID = "NewApp"
CONF_APP_DELETE = "app_delete"
CONF_APP_ID = "app_id"
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("host"): str,
}
)
STEP_PAIR_DATA_SCHEMA = vol.Schema(
{
vol.Required("pin"): str,
@ -66,7 +61,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
"""Handle the initial and reconfigure step."""
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
@ -75,15 +70,32 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
await api.async_generate_cert_if_missing()
self.name, self.mac = await api.async_get_name_and_mac()
await self.async_set_unique_id(format_mac(self.mac))
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data={
CONF_HOST: self.host,
CONF_NAME: self.name,
CONF_MAC: self.mac,
},
)
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
return await self._async_start_pair()
except (CannotConnect, ConnectionClosed):
# Likely invalid IP address or device is network unreachable. Stay
# in the user step allowing the user to enter a different host.
errors["base"] = "cannot_connect"
else:
user_input = {}
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
if self.source == SOURCE_RECONFIGURE:
default_host = self._get_reconfigure_entry().data[CONF_HOST]
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
step_id="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
data_schema=vol.Schema(
{vol.Required(CONF_HOST, default=default_host): str}
),
errors=errors,
)
@ -216,6 +228,12 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration."""
return await self.async_step_user(user_input)
@staticmethod
@callback
def async_get_options_flow(

View File

@ -11,6 +11,15 @@
"host": "The hostname or IP address of the Android TV device."
}
},
"reconfigure": {
"description": "Update the IP address of this previously configured Android TV device.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Android TV device."
}
},
"zeroconf_confirm": {
"title": "Discovered Android TV",
"description": "Do you want to add the Android TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
@ -38,7 +47,9 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
}
},
"options": {

View File

@ -1069,3 +1069,100 @@ async def test_options_flow(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert mock_config_entry.options == {CONF_ENABLE_IME: True}
async def test_reconfigure_flow_success(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
mock_api: MagicMock,
) -> None:
"""Test the full reconfigure flow from start to finish without any exceptions."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert not result["errors"]
assert "host" in result["data_schema"].schema
# Form should have as default value the existing host
host_key = next(k for k in result["data_schema"].schema if k.schema == "host")
assert host_key.default() == mock_config_entry.data["host"]
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
mock_api.async_get_name_and_mac = AsyncMock(
return_value=(mock_config_entry.data["name"], mock_config_entry.data["mac"])
)
# Simulate user input with a new host
new_host = "4.3.2.1"
assert new_host != mock_config_entry.data["host"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": new_host}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data["host"] == new_host
assert len(mock_setup_entry.mock_calls) == 1
async def test_reconfigure_flow_cannot_connect(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
mock_api: MagicMock,
) -> None:
"""Test reconfigure flow with CannotConnect exception."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
mock_api.async_get_name_and_mac = AsyncMock(side_effect=CannotConnect())
new_host = "4.3.2.1"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": new_host}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
assert result["errors"] == {"base": "cannot_connect"}
assert mock_config_entry.data["host"] == "1.2.3.4"
assert len(mock_setup_entry.mock_calls) == 0
async def test_reconfigure_flow_unique_id_mismatch(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
mock_api: MagicMock,
) -> None:
"""Test reconfigure flow with a different device (unique_id mismatch)."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
mock_api.async_generate_cert_if_missing = AsyncMock(return_value=True)
# The new host corresponds to a device with a different MAC/unique_id
new_mac = "FF:EE:DD:CC:BB:AA"
assert new_mac != mock_config_entry.data["mac"]
mock_api.async_get_name_and_mac = AsyncMock(return_value=("name", new_mac))
new_host = "4.3.2.1"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"host": new_host}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unique_id_mismatch"
assert mock_config_entry.data["host"] == "1.2.3.4"
assert len(mock_setup_entry.mock_calls) == 0