diff --git a/homeassistant/components/midea/config_flow.py b/homeassistant/components/midea/config_flow.py index 45fb9b06c2bb..c2e05d2707f8 100644 --- a/homeassistant/components/midea/config_flow.py +++ b/homeassistant/components/midea/config_flow.py @@ -31,7 +31,9 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .const import ( CONF_ACCOUNT, @@ -772,3 +774,29 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=schema, errors={"base": error} if error else None, ) + + @override + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> ConfigFlowResult: + """Handle DHCP discovery of a known Midea 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_IP_ADDRESS] != discovery_info.ip: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_IP_ADDRESS: 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") diff --git a/homeassistant/components/midea/manifest.json b/homeassistant/components/midea/manifest.json index e4f525e2aab8..7a1353ae49d3 100644 --- a/homeassistant/components/midea/manifest.json +++ b/homeassistant/components/midea/manifest.json @@ -3,6 +3,11 @@ "name": "Midea", "codeowners": ["@chemelli74", "@rokam", "@caibinqing"], "config_flow": true, + "dhcp": [ + { + "registered_devices": true + } + ], "documentation": "https://www.home-assistant.io/integrations/midea", "integration_type": "device", "iot_class": "local_polling", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 12352d462bf8..bb1289afb01f 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -651,6 +651,10 @@ DHCP: Final[list[dict[str, str | bool]]] = [ "hostname": "lyric-*", "macaddress": "00D02D*", }, + { + "domain": "midea", + "registered_devices": True, + }, { "domain": "mitsubishi_comfort", "registered_devices": True, diff --git a/tests/components/midea/conftest.py b/tests/components/midea/conftest.py index c47b75e3f5d4..6a0ed19f8cbe 100644 --- a/tests/components/midea/conftest.py +++ b/tests/components/midea/conftest.py @@ -7,9 +7,9 @@ from unittest.mock import AsyncMock, patch from midealocal.const import DeviceType import pytest -from homeassistant.components.midea.const import CONF_KEY, CONF_SUBTYPE, DOMAIN +from homeassistant.components.midea.const import CONF_KEY, CONF_SN, CONF_SUBTYPE, DOMAIN from homeassistant.components.midea.device_catalog import MIDEA_DEVICE_NAMES -from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_TYPE +from homeassistant.const import CONF_MAC, CONF_NAME, CONF_TOKEN, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -179,6 +179,8 @@ def mock_config_entry() -> Callable[[DummyDevice], MockConfigEntry]: **BASE_DATA, CONF_TYPE: device.device_type, CONF_NAME: MIDEA_DEVICE_NAMES[device.device_type], + CONF_MAC: TEST_MAC_ADDRESS, + CONF_SN: TEST_SERIAL_NUMBER, CONF_TOKEN: TEST_TOKEN, CONF_KEY: TEST_KEY, CONF_SUBTYPE: TEST_SUBTYPE, diff --git a/tests/components/midea/const.py b/tests/components/midea/const.py index 1b79090df57f..4bc4d1e5cb24 100644 --- a/tests/components/midea/const.py +++ b/tests/components/midea/const.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_TYPE, ) +TEST_HOSTNAME = "net_ac_2233" TEST_DEVICE_ID = 12345678 TEST_IP_ADDRESS = "1.1.1.1" TEST_KEY = "bb" * 16 diff --git a/tests/components/midea/snapshots/test_diagnostics.ambr b/tests/components/midea/snapshots/test_diagnostics.ambr index d3c0fb464e23..3c8f3eddd7f1 100644 --- a/tests/components/midea/snapshots/test_diagnostics.ambr +++ b/tests/components/midea/snapshots/test_diagnostics.ambr @@ -17,10 +17,12 @@ 'device_id': 12345678, 'ip_address': '1.1.1.1', 'key': '**REDACTED**', + 'mac': '**REDACTED**', 'model': 'MSAGBU-09HRFN8', 'name': 'Air Conditioner', 'port': 6444, 'protocol': 3, + 'sn': '**REDACTED**', 'subtype': 0, 'token': '**REDACTED**', 'type': 172, diff --git a/tests/components/midea/test_config_flow.py b/tests/components/midea/test_config_flow.py index b0fa9a5ef3fe..a86af3682b09 100644 --- a/tests/components/midea/test_config_flow.py +++ b/tests/components/midea/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Midea config flow.""" +from collections.abc import Callable from functools import partial from unittest.mock import AsyncMock, MagicMock, patch @@ -22,7 +23,7 @@ from homeassistant.components.midea.const import ( DOMAIN, ) from homeassistant.components.midea.device_catalog import MIDEA_DEVICE_NAMES -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.const import ( CONF_DEVICE, CONF_DEVICE_ID, @@ -33,17 +34,21 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, + CONF_SOURCE, CONF_TOKEN, CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from .conftest import DummyDevice, default_ac_device from .const import ( BASE_DATA, DISCOVERY_RESULT, EXTENDED_DATA, TEST_DEVICE_ID, + TEST_HOSTNAME, TEST_IP_ADDRESS, TEST_KEY, TEST_MAC_ADDRESS, @@ -2031,3 +2036,79 @@ async def test_auth_method_preset_login_failed(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_method" assert result["errors"] == {"base": "preset_login_failed"} + + +async def test_dhcp_discovery_updates_host( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test DHCP discovery of a known device updates its stored host.""" + config_entry = mock_config_entry(default_ac_device()) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(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=TEST_HOSTNAME, + ip="127.0.0.42", + macaddress=TEST_MAC_ADDRESS.replace(":", ""), + ), + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_IP_ADDRESS] == "127.0.0.42" + + +async def test_dhcp_discovery_same_host( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test DHCP discovery does nothing when the host is already up to date.""" + config_entry = mock_config_entry(default_ac_device()) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(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=TEST_HOSTNAME, + ip=TEST_IP_ADDRESS, + macaddress=TEST_MAC_ADDRESS.replace(":", ""), + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_IP_ADDRESS] == TEST_IP_ADDRESS + + +async def test_dhcp_discovery_no_match( + hass: HomeAssistant, + mock_config_entry: Callable[[DummyDevice], MockConfigEntry], +) -> None: + """Test DHCP discovery aborts when no matching entry is configured.""" + config_entry = mock_config_entry(default_ac_device()) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(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=TEST_HOSTNAME, + ip="1.2.3.4", + macaddress="aabbccddeeff", + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + assert config_entry.data[CONF_IP_ADDRESS] == TEST_IP_ADDRESS