From 14723912f1c5efd574dc047ff9c3010c37fb90cb Mon Sep 17 00:00:00 2001 From: Imou-OpenPlatform Date: Sat, 29 Aug 2026 21:58:35 +0800 Subject: [PATCH] Add DHCP discovery to the Imou integration (#180083) --- homeassistant/components/imou/manifest.json | 13 ++ .../components/imou/quality_scale.yaml | 9 +- homeassistant/components/imou/strings.json | 3 +- homeassistant/generated/dhcp.py | 44 +++++++ tests/components/imou/test_config_flow.py | 114 +++++++++++++++++- 5 files changed, 175 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/imou/manifest.json b/homeassistant/components/imou/manifest.json index 000e1209a6d4..93e177083450 100644 --- a/homeassistant/components/imou/manifest.json +++ b/homeassistant/components/imou/manifest.json @@ -3,6 +3,19 @@ "name": "Imou", "codeowners": ["@Imou-OpenPlatform"], "config_flow": true, + "dhcp": [ + { "macaddress": "1C4D89*" }, + { "macaddress": "302450*" }, + { "macaddress": "38AF29*" }, + { "macaddress": "3CE36B*" }, + { "macaddress": "3CEF8C*" }, + { "macaddress": "906A94*" }, + { "macaddress": "A0BD1D*" }, + { "macaddress": "A83162*" }, + { "macaddress": "AC3DFA*" }, + { "macaddress": "B44C3B*" }, + { "macaddress": "FCB69D*" } + ], "documentation": "https://www.home-assistant.io/integrations/imou", "integration_type": "hub", "iot_class": "cloud_polling", diff --git a/homeassistant/components/imou/quality_scale.yaml b/homeassistant/components/imou/quality_scale.yaml index fce93ed824f2..21e8faea2146 100644 --- a/homeassistant/components/imou/quality_scale.yaml +++ b/homeassistant/components/imou/quality_scale.yaml @@ -48,13 +48,10 @@ rules: diagnostics: todo discovery-update-info: status: exempt - comment: Cloud service integration, does not support discovery. + comment: Cloud hub; DHCP only surfaces the integration. Device IP is not stored or updated. discovery: - status: exempt - comment: >- - Devices are reached via Imou Open Platform cloud APIs (App ID / secret). No - supported local discovery flow today; example cues if investigated later: - hostname `IPC-ABCD.imou.local`, MAC `aa:bb:cc:dd:ee:ff`. + status: done + comment: DHCP matches known Imou / Lechange MAC OUI prefixes. Setup still uses Open Platform App ID and secret. docs-data-update: todo docs-examples: todo docs-known-limitations: todo diff --git a/homeassistant/components/imou/strings.json b/homeassistant/components/imou/strings.json index 52b0f2b3872d..889a603f56b1 100644 --- a/homeassistant/components/imou/strings.json +++ b/homeassistant/components/imou/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d932937f4e51..12352d462bf8 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -504,6 +504,50 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "domain": "iaqualink", "hostname": "iaqualink-*", }, + { + "domain": "imou", + "macaddress": "1C4D89*", + }, + { + "domain": "imou", + "macaddress": "302450*", + }, + { + "domain": "imou", + "macaddress": "38AF29*", + }, + { + "domain": "imou", + "macaddress": "3CE36B*", + }, + { + "domain": "imou", + "macaddress": "3CEF8C*", + }, + { + "domain": "imou", + "macaddress": "906A94*", + }, + { + "domain": "imou", + "macaddress": "A0BD1D*", + }, + { + "domain": "imou", + "macaddress": "A83162*", + }, + { + "domain": "imou", + "macaddress": "AC3DFA*", + }, + { + "domain": "imou", + "macaddress": "B44C3B*", + }, + { + "domain": "imou", + "macaddress": "FCB69D*", + }, { "domain": "incomfort", "hostname": "rfgateway", diff --git a/tests/components/imou/test_config_flow.py b/tests/components/imou/test_config_flow.py index d8d475d76063..d6e8eaad8917 100644 --- a/tests/components/imou/test_config_flow.py +++ b/tests/components/imou/test_config_flow.py @@ -16,14 +16,21 @@ from homeassistant.components.imou.const import ( CONF_APP_SECRET, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import TEST_APP_ID, TEST_APP_SECRET, USER_INPUT from tests.common import MockConfigEntry +DHCP_DISCOVERY = DhcpServiceInfo( + ip="127.0.0.1", + hostname="imou", + macaddress="1c4d895f7a29", +) + async def test_user_flow_success( hass: HomeAssistant, @@ -150,3 +157,108 @@ async def test_user_flow_success_per_region( assert result["title"] == "Imou" assert result["data"] == user_input assert result["result"].unique_id == user_input[CONF_APP_ID] + + +async def test_dhcp_discovery_success( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_imou_openapi_client: AsyncMock, +) -> None: + """DHCP discovery opens the existing user login form and creates an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Imou" + assert result["data"] == USER_INPUT + assert result["result"].unique_id == USER_INPUT[CONF_APP_ID] + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_aborts_when_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Any existing Imou entry suppresses further DHCP discovery.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery_aborts_when_user_flow_in_progress( + hass: HomeAssistant, +) -> None: + """DHCP discovery does not sit beside an unfinished manual setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_dhcp_discovery_invalid_auth( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_imou_openapi_client: AsyncMock, +) -> None: + """Bad credentials stay on the user step, then recover to CREATE_ENTRY.""" + mock_imou_openapi_client.async_get_token.side_effect = ( + InvalidAppIdOrSecretException("fail") + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "invalid_auth" + + mock_imou_openapi_client.async_get_token.reset_mock(side_effect=True) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Imou" + assert result["data"] == USER_INPUT + assert result["result"].unique_id == USER_INPUT[CONF_APP_ID] + assert len(mock_setup_entry.mock_calls) == 1