diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 09eb94d6b07..5257030ecf0 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -11,9 +11,11 @@ from fullykiosk.exceptions import FullyKioskError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from .const import DEFAULT_PORT, DOMAIN, LOGGER @@ -48,7 +50,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device_info["deviceID"]) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=device_info["deviceName"], data=user_input + title=device_info["deviceName"], + data=user_input | {CONF_MAC: format_mac(device_info["Mac"])}, ) return self.async_show_form( @@ -61,3 +64,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery.""" + mac = format_mac(discovery_info.macaddress) + + for entry in self._async_current_entries(): + if entry.data[CONF_MAC] == mac: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="unknown") diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index 4e50bb6efe6..7be06c79573 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -1,6 +1,7 @@ """Base entity for the Fully Kiosk Browser integration.""" from __future__ import annotations +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,4 +24,5 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit model=coordinator.data["deviceModel"], sw_version=coordinator.data["appVersionName"], configuration_url=f"http://{coordinator.data['ip4']}:2323", + connections={(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])}, ) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 40c7e5293e7..8918ce28062 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -6,5 +6,6 @@ "requirements": ["python-fullykiosk==0.0.11"], "dependencies": [], "codeowners": ["@cgarwood"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [{ "registered_devices": true }] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9179a314215..841db03c3a6 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -43,6 +43,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'flux_led', 'hostname': 'zengge_[0-9a-f][0-9a-f]_*'}, {'domain': 'flux_led', 'hostname': 'sta*', 'macaddress': 'C82E47*'}, {'domain': 'fronius', 'macaddress': '0003AC*'}, + {'domain': 'fully_kiosk', 'registered_devices': True}, {'domain': 'goalzero', 'registered_devices': True}, {'domain': 'goalzero', 'hostname': 'yeti*'}, {'domain': 'gogogate2', 'hostname': 'ismartgate*'}, diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index c5476ea6a9d..35d5c12e694 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -20,7 +20,11 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Test device", domain=DOMAIN, - data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"}, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, unique_id="12345", ) @@ -45,6 +49,7 @@ def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = { "deviceName": "Test device", "deviceID": "12345", + "Mac": "AA:BB:CC:DD:EE:FF", } yield client diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 2617a3f7adb..19a8715b4cd 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -7,9 +7,10 @@ from aiohttp.client_exceptions import ClientConnectorError from fullykiosk import FullyKioskError import pytest +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -42,6 +43,7 @@ async def test_full_flow( assert result2.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result2 assert result2["result"].unique_id == "12345" @@ -95,6 +97,7 @@ async def test_errors( assert result3.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result3 assert result3["result"].unique_id == "12345" @@ -131,6 +134,54 @@ async def test_duplicate_updates_existing_entry( assert mock_config_entry.data == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + + +async def test_dhcp_discovery_updates_entry( + hass: HomeAssistant, + 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": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + + +async def test_dhcp_unknown_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unknown DHCP discovery aborts flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:00", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "unknown"