From 757deb3a1cbbda59f710e392f67c436f3f09c3cb Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:29:38 +0200 Subject: [PATCH] Add reconfiguration flow to Notifications for Android TV / Fire TV (#169111) --- .../components/nfandroidtv/config_flow.py | 27 ++++- .../components/nfandroidtv/strings.json | 12 +- tests/components/nfandroidtv/conftest.py | 37 +++++++ .../nfandroidtv/test_config_flow.py | 103 +++++++++++++++++- 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 tests/components/nfandroidtv/conftest.py diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py index 9a5e420bbdc..34893702b5d 100644 --- a/homeassistant/components/nfandroidtv/config_flow.py +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -9,7 +9,7 @@ from notifications_android_tv.notifications import ConnectError, Notifications import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME from .const import DEFAULT_NAME, DOMAIN @@ -40,6 +40,31 @@ class NFAndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure flow for Notification for Android TV / Fire TV.""" + errors: dict[str, str] = {} + entry = self._get_reconfigure_entry() + + if user_input is not None: + self._async_abort_entries_match(user_input) + if not (error := await self._async_try_connect(user_input[CONF_HOST])): + return self.async_update_reload_and_abort( + entry, data_updates=user_input + ) + errors["base"] = error + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + suggested_values=user_input or entry.data, + ), + description_placeholders={CONF_NAME: entry.title}, + errors=errors, + ) + async def _async_try_connect(self, host: str) -> str | None: """Try connecting to Android TV / Fire TV.""" try: diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index 531a6af1617..79c9648942b 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -1,13 +1,23 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::nfandroidtv::config::step::user::data_description::host%]" + }, + "description": "Reconfigure {name}" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", diff --git a/tests/components/nfandroidtv/conftest.py b/tests/components/nfandroidtv/conftest.py new file mode 100644 index 00000000000..6129b36ce93 --- /dev/null +++ b/tests/components/nfandroidtv/conftest.py @@ -0,0 +1,37 @@ +"""Common fixtures for the Notifications for Android TV / Fire TV tests.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.nfandroidtv.const import DOMAIN +from homeassistant.const import CONF_HOST + +from . import HOST, NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_notifications_android_tv() -> Generator[MagicMock]: + """Mock notifications_android_tv.""" + + with patch( + "homeassistant.components.nfandroidtv.config_flow.Notifications", autospec=True + ) as mock_client: + client = mock_client.return_value + client.cls = mock_client + + yield client + + +@pytest.fixture(name="config_entry") +def mock_config_entry() -> MockConfigEntry: + """Mock Notifications for Android TV / Fire TV configuration entry.""" + return MockConfigEntry( + domain=DOMAIN, + title=NAME, + data={CONF_HOST: HOST}, + entry_id="123456789", + ) diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index 271961fbee7..2daa18eb524 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -1,11 +1,13 @@ """Test NFAndroidTV config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from notifications_android_tv.notifications import ConnectError +import pytest from homeassistant import config_entries from homeassistant.components.nfandroidtv.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -97,3 +99,102 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} + + +@pytest.mark.usefixtures("mock_notifications_android_tv") +async def test_flow_reconfigure( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow.""" + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "4.3.2.1"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_HOST] == "4.3.2.1" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), [(ConnectError, "cannot_connect"), (ValueError, "unknown")] +) +async def test_flow_reconfigure_errors( + hass: HomeAssistant, + mock_notifications_android_tv: MagicMock, + config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reconfigure flow errors.""" + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_notifications_android_tv.cls.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "4.3.2.1"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_notifications_android_tv.cls.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "4.3.2.1"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_HOST] == "4.3.2.1" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.usefixtures("mock_notifications_android_tv") +async def test_flow_reconfigure_already_configured( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow aborts if already configured.""" + MockConfigEntry( + domain=DOMAIN, + title="Android TV / Fire TV (4.3.2.1)", + data={CONF_HOST: "4.3.2.1"}, + entry_id="987654321", + ).add_to_hass(hass) + + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "4.3.2.1"}, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert len(hass.config_entries.async_entries()) == 2