File: /volume1/docker/homeassistant2025/custom_components/midea_dehumidifier_lan/__init__.py
"""
The custom component for local network access to Midea appliances
"""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_VERSION,
CONF_BROADCAST_ADDRESS,
CONF_DEVICES,
CONF_DISCOVERY,
CONF_EXCLUDE,
CONF_ID,
CONF_IP_ADDRESS,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
CONF_TTL,
CONF_TYPE,
CONF_UNIQUE_ID,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import async_get
from midea_beautiful.cloud import MideaCloud
from midea_beautiful.exceptions import MideaError
from midea_beautiful.lan import LanDevice
from midea_beautiful.midea import SUPPORTED_APPS, DEFAULT_APP_ID, DEFAULT_APPKEY
from custom_components.midea_dehumidifier_lan.const import (
CONF_MOBILE_APP,
CONF_TOKEN_KEY,
CONF_USE_CLOUD_OBSOLETE,
DEFAULT_APP,
DEFAULT_TTL,
DISCOVERY_CLOUD,
DISCOVERY_IGNORE,
DISCOVERY_LAN,
DISCOVERY_WAIT,
DOMAIN,
LOCAL_BROADCAST,
NAME,
CURRENT_CONFIG_VERSION,
OBSOLETE_CONF_APPID,
OBSOLETE_CONF_APPKEY,
PLATFORMS,
UNKNOWN_IP,
)
from custom_components.midea_dehumidifier_lan.hub import Hub
from custom_components.midea_dehumidifier_lan.util import MideaClient, address_ok
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up platform from a ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
if (hub := hass.data[DOMAIN].get(config_entry.entry_id)) is None:
hub = Hub(hass, config_entry)
hass.data[DOMAIN][config_entry.entry_id] = hub
await hub.async_setup()
await _async_migrate_names(hass, config_entry)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def _async_migrate_names(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
entity_registry = async_get(hass)
conf = config_entry.data
if devices := conf.get(CONF_DEVICES):
old_entites = [
entry
for _, entry in entity_registry.entities.items()
if entry.platform == DOMAIN
]
for reg_entry in old_entites:
for device in devices:
old_suffix = f"_{device[CONF_ID]}"
new_suffix = f"_{device[CONF_UNIQUE_ID]}"
if reg_entry.unique_id.endswith(old_suffix):
prefix = reg_entry.unique_id[: -len(old_suffix)]
old_unique_id = reg_entry.unique_id
new_unique_id = f"{prefix}{new_suffix}"
try:
entity_registry.async_update_entity(
reg_entry.entity_id,
new_unique_id=new_unique_id,
)
_LOGGER.warning(
"Changed unique id of %s from %s to %s",
reg_entry.entity_id,
old_unique_id,
new_unique_id,
)
except ValueError as ex:
_LOGGER.error(
"Unable to change unique id of %s: %s",
reg_entry.entity_id,
ex,
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hub: Hub = hass.data[DOMAIN].pop(entry.entry_id)
await hub.async_unload()
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old config entry to new version."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version < CURRENT_CONFIG_VERSION:
old_conf = config_entry.data
old_broadcast = old_conf.get(CONF_BROADCAST_ADDRESS, [])
if not old_broadcast:
old_broadcast = [LOCAL_BROADCAST]
new_conf = {
CONF_MOBILE_APP: old_conf.get(CONF_MOBILE_APP),
CONF_BROADCAST_ADDRESS: old_broadcast,
CONF_USERNAME: old_conf.get(CONF_USERNAME),
CONF_PASSWORD: old_conf.get(CONF_PASSWORD),
}
if not old_conf.get(OBSOLETE_CONF_APPID) or not old_conf.get(
OBSOLETE_CONF_APPKEY
):
new_conf[CONF_MOBILE_APP] = DEFAULT_APP
else:
appkey = old_conf.get(OBSOLETE_CONF_APPKEY, DEFAULT_APPKEY)
if appkey:
for appname, appconf in SUPPORTED_APPS.items():
if appconf["appkey"] == appkey:
new_conf[CONF_MOBILE_APP] = appname
break
else:
appid = old_conf.get(OBSOLETE_CONF_APPID, DEFAULT_APP_ID)
for appname, appconf in SUPPORTED_APPS.items():
if appconf["appid"] == appid:
new_conf[CONF_MOBILE_APP] = appname
break
new_devices = []
new_conf[CONF_DEVICES] = new_devices
id_resolver = _ApplianceIdResolver(hass)
old: dict[str, Any]
for old in config_entry.data[CONF_DEVICES]:
new = {
CONF_API_VERSION: old.get(CONF_API_VERSION),
CONF_DISCOVERY: old.get(CONF_DISCOVERY),
CONF_ID: old.get(CONF_ID),
CONF_IP_ADDRESS: old.get(CONF_IP_ADDRESS, UNKNOWN_IP),
CONF_NAME: old.get(CONF_NAME),
CONF_TOKEN: old.get(CONF_TOKEN),
CONF_TOKEN_KEY: old.get(CONF_TOKEN_KEY),
CONF_TYPE: old.get(CONF_TYPE),
CONF_UNIQUE_ID: old.get(CONF_UNIQUE_ID),
CONF_TTL: old.get(CONF_TTL, DEFAULT_TTL),
}
discovery_mode = new.get(CONF_DISCOVERY)
if discovery_mode not in [
DISCOVERY_WAIT,
DISCOVERY_LAN,
DISCOVERY_IGNORE,
DISCOVERY_CLOUD,
]:
if old.get(CONF_USE_CLOUD_OBSOLETE):
new[CONF_DISCOVERY] = DISCOVERY_CLOUD
elif old.get(CONF_EXCLUDE):
new[CONF_DISCOVERY] = DISCOVERY_IGNORE
elif not address_ok(old.get(CONF_IP_ADDRESS)):
new[CONF_DISCOVERY] = DISCOVERY_WAIT
else:
new[CONF_DISCOVERY] = DISCOVERY_LAN
await id_resolver.async_get_unique_id_if_missing(new_conf, new)
new_devices.append(new)
config_entry.version = CURRENT_CONFIG_VERSION
_LOGGER.debug(
"Migrating configuration from %s to %s", config_entry.data, new_conf
)
if hass.config_entries.async_update_entry(
config_entry, data=new_conf, title=NAME
):
_LOGGER.info("Configuration migrated to version %s", config_entry.version)
else:
_LOGGER.debug(
"Configuration didn't change during migration to version %s",
config_entry.version,
)
return id_resolver.success
return True
# pylint: disable=too-few-public-methods
class _ApplianceIdResolver:
def __init__(self, hass: HomeAssistant) -> None:
self.hass = hass
self.client = MideaClient(hass)
self.cloud: MideaCloud | None = None
self.descriptors: list[dict] | None = None
self.success = True
async def _start(self, conf: dict[str, Any]) -> None:
try:
self.cloud = await self.client.async_connect_to_cloud(conf)
self.descriptors = await self.client.async_list_appliances(self.cloud)
except MideaError as ex:
_LOGGER.error(
"Unable to get list of appliances during configuration migration %s.",
ex,
exc_info=True,
)
async def async_get_unique_id_if_missing(
self,
conf: dict[str, Any],
device_conf: dict[str, Any],
) -> None:
"""If there is no unique_id assigned, try to find serial number"""
if device_conf[CONF_UNIQUE_ID] is None:
if device_conf[CONF_DISCOVERY] == DISCOVERY_LAN:
appliance = await self._get_appliance_state(device_conf)
device_conf[CONF_UNIQUE_ID] = appliance and appliance.serial_number
if device_conf[CONF_UNIQUE_ID] is None:
if self.cloud is None:
await self._start(conf)
self._find_unique_id_in_appliance_list(device_conf)
if device_conf[CONF_UNIQUE_ID] is None:
_LOGGER.error(
"Unable to find serial number for appliance %s."
"Please re-install %s integration.",
device_conf[CONF_NAME],
NAME,
)
self.success = False
def _find_unique_id_in_appliance_list(self, device_conf) -> None:
if self.descriptors is not None:
for app in self.descriptors:
if app["id"] == device_conf[CONF_ID]:
if app["sn"] and app["sn"] != "Unknown":
device_conf[CONF_UNIQUE_ID] = app["sn"]
else:
_LOGGER.warning("Unable to get serial number for %s", app)
break
async def _get_appliance_state(
self,
device_conf: dict[str, Any],
cloud: MideaCloud = None,
use_cloud: bool = False,
) -> LanDevice | None:
try:
return await self.hass.async_add_executor_job(
self.client.appliance_state,
device_conf[CONF_IP_ADDRESS],
device_conf[CONF_TOKEN],
device_conf[CONF_TOKEN_KEY],
cloud,
use_cloud,
device_conf[CONF_ID],
)
except MideaError as ex:
_LOGGER.error(
"Unable to poll appliance during configuration migration %s.",
ex,
exc_info=True,
)
return None