From 322dc2adeb4dd3b7825205db8c32d88c4ddacc5c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 11 Apr 2026 22:26:22 +0200 Subject: [PATCH] Add DHCP discovery for known Elgato devices (#168002) --- .../components/elgato/config_flow.py | 29 ++++++- homeassistant/components/elgato/manifest.json | 5 ++ .../components/elgato/quality_scale.yaml | 6 +- homeassistant/components/elgato/strings.json | 3 +- homeassistant/generated/dhcp.py | 4 + tests/components/elgato/test_config_flow.py | 79 ++++++++++++++++++- 6 files changed, 118 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index a47f039384ca..e2832554a221 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DOMAIN @@ -23,7 +25,6 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 host: str - port: int serial_number: str mac: str | None = None @@ -70,6 +71,32 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initiated by zeroconf.""" return self._async_create_entry() + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle DHCP discovery of a known Elgato device. + + Only devices already configured (matched via ``registered_devices``) + reach this step. It is used to keep the stored host in sync with the + current IP address of the device. + """ + mac = format_mac(discovery_info.macaddress) + + for entry in self._async_current_entries(): + if (entry_mac := entry.data.get(CONF_MAC)) is None or format_mac( + entry_mac + ) != mac: + continue + if entry.data[CONF_HOST] != discovery_info.ip: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.config_entries.async_schedule_reload(entry.entry_id) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="no_devices_found") + @callback def _async_show_setup_form( self, errors: dict[str, str] | None = None diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json index 734ad5ec9300..5717c822a333 100644 --- a/homeassistant/components/elgato/manifest.json +++ b/homeassistant/components/elgato/manifest.json @@ -3,6 +3,11 @@ "name": "Elgato Light", "codeowners": ["@frenck"], "config_flow": true, + "dhcp": [ + { + "registered_devices": true + } + ], "documentation": "https://www.home-assistant.io/integrations/elgato", "integration_type": "device", "iot_class": "local_polling", diff --git a/homeassistant/components/elgato/quality_scale.yaml b/homeassistant/components/elgato/quality_scale.yaml index 531f0447f708..b1a881827a57 100644 --- a/homeassistant/components/elgato/quality_scale.yaml +++ b/homeassistant/components/elgato/quality_scale.yaml @@ -39,11 +39,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: todo docs-examples: todo diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json index 18bd15683366..afa7f303ee3a 100644 --- a/homeassistant/components/elgato/strings.json +++ b/homeassistant/components/elgato/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8fa8aff1b186..75b08711f1f4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -173,6 +173,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "dlink", "hostname": "dsp-w215", }, + { + "domain": "elgato", + "registered_devices": True, + }, { "domain": "elkm1", "registered_devices": True, diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index c647d36902a0..e3ed2c10818a 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -7,10 +7,11 @@ from elgato import ElgatoConnectionError import pytest from homeassistant.components.elgato.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from tests.common import MockConfigEntry @@ -254,3 +255,79 @@ async def test_zeroconf_during_onboarding( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_elgato.info.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 + + +@pytest.mark.usefixtures("mock_elgato") +async def test_dhcp_discovery_updates_host( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery of a known device updates its stored host.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="elgato", + ip="127.0.0.42", + macaddress="aabbccddeeff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.42" + + +@pytest.mark.usefixtures("mock_elgato") +async def test_dhcp_discovery_same_host( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery does nothing when the host is already up to date.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="elgato", + ip="127.0.0.1", + macaddress="aabbccddeeff", + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1" + + +@pytest.mark.usefixtures("mock_elgato") +async def test_dhcp_discovery_no_match( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery aborts when no matching entry is configured.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="elgato", + ip="127.0.0.42", + macaddress="001122334455", + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.1"