From a9cab284748c115d1199fdd5487e9c002b9b4110 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:17:04 +0100 Subject: [PATCH] Add DHCP configuration update in HomeWizard (#131547) --- .../components/homewizard/config_flow.py | 27 +++++++ .../components/homewizard/manifest.json | 5 ++ .../components/homewizard/quality_scale.yaml | 6 +- homeassistant/generated/dhcp.py | 4 + .../components/homewizard/test_config_flow.py | 76 ++++++++++++++++++- 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index aab6ce055a2..21d264030cf 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -12,6 +12,7 @@ from homewizard_energy.v1.models import Device from voluptuous import Required, Schema from homeassistant.components import onboarding, zeroconf +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH from homeassistant.data_entry_flow import AbortFlow @@ -110,6 +111,32 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle dhcp discovery to update existing entries. + + This flow is triggered only by DHCP discovery of known devices. + """ + try: + device = await self._async_try_connect(discovery_info.ip) + except RecoverableError as ex: + _LOGGER.error(ex) + return self.async_abort(reason="unknown") + + await self.async_set_unique_id( + f"{device.product_type}_{discovery_info.macaddress}" + ) + + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: discovery_info.ip} + ) + + # This situation should never happen, as Home Assistant will only + # send updates for existing entries. In case it does, we'll just + # abort the flow with an unknown error. + return self.async_abort(reason="unknown") + async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 0ba2ac0eea7..78b80dd788e 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -3,6 +3,11 @@ "name": "HomeWizard Energy", "codeowners": ["@DCSBL"], "config_flow": true, + "dhcp": [ + { + "registered_devices": true + } + ], "documentation": "https://www.home-assistant.io/integrations/homewizard", "iot_class": "local_polling", "loggers": ["homewizard_energy"], diff --git a/homeassistant/components/homewizard/quality_scale.yaml b/homeassistant/components/homewizard/quality_scale.yaml index 1dbdba8212d..e71c4aa8c18 100644 --- a/homeassistant/components/homewizard/quality_scale.yaml +++ b/homeassistant/components/homewizard/quality_scale.yaml @@ -46,11 +46,7 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: - status: todo - comment: | - The integration doesn't update the device info based on DHCP discovery - of known existing devices. + discovery-update-info: done discovery: done docs-data-update: done docs-examples: done diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 1ef91841db8..e37fb2332b1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -236,6 +236,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "guardian*", "macaddress": "30AEA4*", }, + { + "domain": "homewizard", + "registered_devices": True, + }, { "domain": "hunterdouglas_powerview", "registered_devices": True, diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 84bdb0ba921..5ae0e0ef88a 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant @@ -263,6 +263,80 @@ async def test_discovery_invalid_api(hass: HomeAssistant) -> None: assert result["reason"] == "unsupported_api_version" +async def test_dhcp_discovery_updates_entry( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery updates config entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.0.0.127", + hostname="HW-p1meter-aabbcc", + macaddress="5c2fafabcdef", + ), + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data[CONF_IP_ADDRESS] == "1.0.0.127" + + +@pytest.mark.usefixtures("mock_setup_entry") +@pytest.mark.parametrize( + ("exception"), + [(DisabledError), (RequestError)], +) +async def test_dhcp_discovery_updates_entry_fails( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, + mock_config_entry: MockConfigEntry, + exception: Exception, +) -> None: + """Test DHCP discovery updates config entries, but fails to connect.""" + mock_homewizardenergy.device.side_effect = exception + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.0.0.127", + hostname="HW-p1meter-aabbcc", + macaddress="5c2fafabcdef", + ), + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "unknown" + + +async def test_dhcp_discovery_ignores_unknown( + hass: HomeAssistant, + mock_homewizardenergy: MagicMock, +) -> None: + """Test DHCP discovery is only used for updates. + + Anything else will just abort the flow. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="127.0.0.1", + hostname="HW-p1meter-aabbcc", + macaddress="5c2fafabcdef", + ), + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "unknown" + + async def test_discovery_flow_updates_new_ip( hass: HomeAssistant, mock_config_entry: MockConfigEntry,