Fix deviceinfo missing mac and s/n for Midea (#180594)

This commit is contained in:
Simone Chemelli
2026-08-29 13:01:46 +02:00
committed by GitHub
parent 9575dd00a7
commit 51765a13c1
9 changed files with 189 additions and 14 deletions
+41 -1
View File
@@ -1,6 +1,7 @@
"""The Midea integration."""
from collections.abc import Mapping
from functools import partial
from typing import Any
from midealocal.const import ProtocolVersion
@@ -11,6 +12,7 @@ from midealocal.discover import discover
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PORT,
@@ -21,8 +23,9 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_KEY, CONF_SUBTYPE, LOGGER
from .const import CONF_KEY, CONF_SN, CONF_SUBTYPE, LOGGER
from .entity import MideaConfigEntry
_PLATFORMS: list[Platform] = [
@@ -133,6 +136,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> boo
return True
async def async_migrate_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> bool:
"""Migrate an old config entry."""
if entry.minor_version < 2:
# minor_version 2 adds mac/serial number; discovery already parses both
# from the device reply, so backfill them for pre-existing entries.
discovered = await hass.async_add_executor_job(
partial(discover, ip_address=entry.data[CONF_IP_ADDRESS])
)
device = discovered.get(entry.data[CONF_DEVICE_ID], {})
new_data = {**entry.data}
if mac := device.get(CONF_MAC):
new_data[CONF_MAC] = mac
if serial_number := device.get(CONF_SN):
new_data[CONF_SN] = serial_number
# Drop any network-mac connection with an empty value so only a real
# address remains; the device registry never removes these on its own.
device_registry = dr.async_get(hass)
for device_entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
):
connections = {
connection
for connection in device_entry.connections
if connection[0] != dr.CONNECTION_NETWORK_MAC
or str(connection[1]).lower() not in ("", "none")
}
if connections != device_entry.connections:
device_registry.async_update_device(
device_entry.id, new_connections=connections
)
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
return True
async def async_unload_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
+24 -2
View File
@@ -21,6 +21,7 @@ from homeassistant.const import (
CONF_DEVICE,
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
@@ -32,7 +33,15 @@ from homeassistant.const import (
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
from .const import CONF_ACCOUNT, CONF_KEY, CONF_SERVER, CONF_SUBTYPE, DOMAIN, LOGGER
from .const import (
CONF_ACCOUNT,
CONF_KEY,
CONF_SERVER,
CONF_SN,
CONF_SUBTYPE,
DOMAIN,
LOGGER,
)
from .device_catalog import MIDEA_DEVICE_NAMES
DEFAULT_CLOUD: str = get_default_cloud()
@@ -91,7 +100,7 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
"""
VERSION = 1
MINOR_VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""MideaConfigFlow class."""
@@ -456,6 +465,8 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IP_ADDRESS: device.get(CONF_IP_ADDRESS),
CONF_PORT: device.get(CONF_PORT),
CONF_MODEL: device.get(CONF_MODEL),
CONF_MAC: device.get(CONF_MAC),
CONF_SN: device.get(CONF_SN),
}
# MUST get a auth passed token/key for v3 device, disable add before pass
@@ -541,6 +552,8 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_SUBTYPE: self.found_device.get(CONF_SUBTYPE) or 0,
CONF_TOKEN: self.found_device.get(CONF_TOKEN) or "",
CONF_KEY: self.found_device.get(CONF_KEY) or "",
CONF_MAC: self.found_device.get(CONF_MAC),
CONF_SN: self.found_device.get(CONF_SN),
}
async def _async_create_midea_entry(
@@ -589,6 +602,10 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_KEY: user_input[CONF_KEY],
}
if mac := user_input.get(CONF_MAC):
data[CONF_MAC] = mac
if serial_number := user_input.get(CONF_SN):
data[CONF_SN] = serial_number
return self.async_create_entry(
title=name,
@@ -679,6 +696,9 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
user_input[CONF_KEY] = keys["key"]
user_input[CONF_TOKEN] = keys["token"]
user_input[CONF_MAC] = device.get(CONF_MAC)
user_input[CONF_SN] = device.get(CONF_SN)
self.found_device = {
CONF_DEVICE_ID: user_input[CONF_DEVICE_ID],
CONF_NAME: self.found_device.get(CONF_NAME),
@@ -689,6 +709,8 @@ class MideaConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_MODEL: user_input[CONF_MODEL],
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_KEY: user_input[CONF_KEY],
CONF_MAC: user_input[CONF_MAC],
CONF_SN: user_input[CONF_SN],
}
return await self._async_create_midea_entry(user_input)
+1
View File
@@ -12,6 +12,7 @@ CONF_KEY = "key"
CONF_SUBTYPE = "subtype"
CONF_ACCOUNT = "account"
CONF_SERVER = "server"
CONF_SN = "sn"
PRESET_MODE_NONE = "none"
@@ -3,13 +3,13 @@
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_TOKEN
from homeassistant.const import CONF_MAC, CONF_TOKEN
from homeassistant.core import HomeAssistant
from .const import CONF_KEY
from .const import CONF_KEY, CONF_SN
from .entity import MideaConfigEntry
TO_REDACT = {CONF_TOKEN, CONF_KEY}
TO_REDACT = {CONF_TOKEN, CONF_KEY, CONF_MAC, CONF_SN}
async def async_get_config_entry_diagnostics(
+1
View File
@@ -174,6 +174,7 @@ def mock_config_entry() -> Callable[[DummyDevice], MockConfigEntry]:
def _create(device: DummyDevice) -> MockConfigEntry:
return MockConfigEntry(
domain=DOMAIN,
minor_version=2,
data={
**BASE_DATA,
CONF_TYPE: device.device_type,
+4 -1
View File
@@ -2,11 +2,12 @@
from midealocal.const import ProtocolVersion
from homeassistant.components.midea.const import CONF_KEY, CONF_SUBTYPE
from homeassistant.components.midea.const import CONF_KEY, CONF_SN, CONF_SUBTYPE
from homeassistant.components.midea.device_catalog import MIDEA_DEVICE_NAMES
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_PORT,
CONF_PROTOCOL,
@@ -38,6 +39,8 @@ DISCOVERY_RESULT = {
TEST_DEVICE_ID: {
**BASE_DATA,
CONF_TYPE: TEST_TYPE,
CONF_MAC: TEST_MAC_ADDRESS,
CONF_SN: TEST_SERIAL_NUMBER,
}
}
@@ -29,7 +29,7 @@
'discovery_keys': dict({
}),
'domain': 'midea',
'minor_version': 1,
'minor_version': 2,
'options': dict({
}),
'pref_disable_new_entities': False,
+15 -1
View File
@@ -17,6 +17,7 @@ from homeassistant.components.midea.const import (
CONF_ACCOUNT,
CONF_KEY,
CONF_SERVER,
CONF_SN,
CONF_SUBTYPE,
DOMAIN,
)
@@ -26,6 +27,7 @@ from homeassistant.const import (
CONF_DEVICE,
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PASSWORD,
@@ -44,9 +46,11 @@ from .const import (
TEST_DEVICE_ID,
TEST_IP_ADDRESS,
TEST_KEY,
TEST_MAC_ADDRESS,
TEST_MODEL,
TEST_PORT,
TEST_PROTOCOL,
TEST_SERIAL_NUMBER,
TEST_SUBTYPE,
TEST_TOKEN,
TEST_TYPE,
@@ -104,6 +108,8 @@ async def test_manual_flow_success(hass: HomeAssistant) -> None:
CONF_SUBTYPE: TEST_SUBTYPE,
CONF_TOKEN: TEST_TOKEN,
CONF_KEY: TEST_KEY,
CONF_MAC: TEST_MAC_ADDRESS,
CONF_SN: TEST_SERIAL_NUMBER,
}
@@ -1056,7 +1062,13 @@ async def test_auto_flow_v1_v2_success_when_cloud_down(
) -> None:
"""Test v1/v2 devices are added without ever using the cloud, even if it is down."""
mock_devices = {
TEST_DEVICE_ID: {**BASE_DATA, CONF_TYPE: TEST_TYPE, CONF_PROTOCOL: protocol},
TEST_DEVICE_ID: {
**BASE_DATA,
CONF_TYPE: TEST_TYPE,
CONF_PROTOCOL: protocol,
CONF_MAC: TEST_MAC_ADDRESS,
CONF_SN: TEST_SERIAL_NUMBER,
},
}
result = await hass.config_entries.flow.async_init(
@@ -1105,6 +1117,8 @@ async def test_auto_flow_v1_v2_success_when_cloud_down(
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_DEVICE_ID] == TEST_DEVICE_ID
assert result["data"][CONF_PROTOCOL] == protocol
assert result["data"][CONF_MAC] == TEST_MAC_ADDRESS
assert result["data"][CONF_SN] == TEST_SERIAL_NUMBER
async def test_login_credentials_step_renders_with_cloud_servers(
+99 -5
View File
@@ -4,11 +4,12 @@ from unittest.mock import patch
from midealocal.const import DeviceType, ProtocolVersion
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.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PORT,
@@ -17,10 +18,11 @@ from homeassistant.const import (
CONF_TYPE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .conftest import DummyDevice
from .const import TEST_DEVICE_ID
from .const import TEST_DEVICE_ID, TEST_IP_ADDRESS, TEST_MAC_ADDRESS, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry
@@ -45,7 +47,7 @@ async def test_async_setup(hass: HomeAssistant) -> None:
async def test_unload_entry(hass: HomeAssistant) -> None:
"""Test async_unload_entry unloads platforms and closes the device."""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA)
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2)
entry.add_to_hass(hass)
device = DummyDevice(DeviceType.AC)
with patch(
@@ -62,7 +64,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
async def test_async_setup_entry_paths(hass: HomeAssistant) -> None:
"""Test async_setup_entry for success and no-device return."""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA)
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.midea.device_selector",
@@ -74,6 +76,7 @@ async def test_async_setup_entry_paths(hass: HomeAssistant) -> None:
entry2 = MockConfigEntry(
domain=DOMAIN,
data={**_ENTRY_DATA, CONF_DEVICE_ID: TEST_DEVICE_ID + 1},
minor_version=2,
)
entry2.add_to_hass(hass)
with patch(
@@ -94,7 +97,7 @@ async def test_setup_entry_not_ready_on_connect_failure(
It can also leave the socket open in that case (e.g. when authentication
fails), so the socket must be closed explicitly to avoid a ResourceWarning.
"""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA)
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=2)
entry.add_to_hass(hass)
device = DummyDevice(DeviceType.AC)
with (
@@ -164,3 +167,94 @@ async def test_setup_entry_no_recovery_when_device_not_discovered(
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_RETRY
assert entry.data[CONF_IP_ADDRESS] == _ENTRY_DATA[CONF_IP_ADDRESS]
async def test_migrate_entry_backfills_mac_and_serial_number(
hass: HomeAssistant,
) -> None:
"""Test migration to minor_version 2 backfills mac and serial number."""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1)
entry.add_to_hass(hass)
with (
patch(
"homeassistant.components.midea.discover",
return_value={
TEST_DEVICE_ID: {
CONF_IP_ADDRESS: TEST_IP_ADDRESS,
CONF_MAC: TEST_MAC_ADDRESS,
CONF_SN: TEST_SERIAL_NUMBER,
}
},
),
patch(
"homeassistant.components.midea.device_selector",
return_value=DummyDevice(DeviceType.AC),
),
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
assert entry.minor_version == 2
assert entry.data[CONF_MAC] == TEST_MAC_ADDRESS
assert entry.data[CONF_SN] == TEST_SERIAL_NUMBER
async def test_migrate_entry_drops_empty_mac_connection(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test migration removes a leftover empty network-mac connection."""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1)
entry.add_to_hass(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, str(TEST_DEVICE_ID))},
connections={
(dr.CONNECTION_NETWORK_MAC, "None"),
(dr.CONNECTION_NETWORK_MAC, TEST_MAC_ADDRESS),
},
)
with (
patch(
"homeassistant.components.midea.discover",
return_value={
TEST_DEVICE_ID: {
CONF_IP_ADDRESS: TEST_IP_ADDRESS,
CONF_MAC: TEST_MAC_ADDRESS,
CONF_SN: TEST_SERIAL_NUMBER,
}
},
),
patch(
"homeassistant.components.midea.device_selector",
return_value=DummyDevice(DeviceType.AC),
),
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
device_entry = device_registry.async_get_device_by_identifier(
(DOMAIN, str(TEST_DEVICE_ID)), entry.entry_id
)
assert device_entry is not None
assert device_entry.connections == {(dr.CONNECTION_NETWORK_MAC, TEST_MAC_ADDRESS)}
async def test_migrate_entry_without_discovery_result(hass: HomeAssistant) -> None:
"""Test migration still completes when discovery finds nothing."""
entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, minor_version=1)
entry.add_to_hass(hass)
with (
patch("homeassistant.components.midea.discover", return_value={}),
patch(
"homeassistant.components.midea.device_selector",
return_value=DummyDevice(DeviceType.AC),
),
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.LOADED
assert entry.minor_version == 2
assert CONF_MAC not in entry.data
assert CONF_SN not in entry.data