Files
core/homeassistant/components/midea/__init__.py
T

179 lines
6.1 KiB
Python

"""The Midea integration."""
from collections.abc import Mapping
from functools import partial
from typing import Any
from midealocal.const import ProtocolVersion
from midealocal.device import MideaDevice
from midealocal.devices import device_selector
from midealocal.discover import discover
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_IP_ADDRESS,
CONF_MAC,
CONF_MODEL,
CONF_NAME,
CONF_PORT,
CONF_PROTOCOL,
CONF_TOKEN,
CONF_TYPE,
Platform,
)
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_SN, CONF_SUBTYPE, LOGGER
from .entity import MideaConfigEntry
_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.FAN,
Platform.HUMIDIFIER,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
]
def _create_device(data: Mapping[str, Any], ip_address: str) -> MideaDevice | None:
"""Create the device object for the given entry data and IP address."""
return device_selector(
data[CONF_NAME],
data[CONF_DEVICE_ID],
data[CONF_TYPE],
ip_address,
data[CONF_PORT],
data[CONF_TOKEN],
data[CONF_KEY],
ProtocolVersion(data[CONF_PROTOCOL]),
data[CONF_MODEL],
data[CONF_SUBTYPE],
"",
)
def _connect(device: MideaDevice) -> bool:
"""Connect to the device, always closing the socket on failure.
connect() swallows AuthException/SocketException internally and can
leave the socket open even though it reports failure, so it must be
closed explicitly here to avoid a ResourceWarning.
"""
connected = device.connect(True)
if not connected:
device.close_socket()
return connected
def _discover_current_ip(device_id: int) -> str | None:
"""Look up the device's current IP address via local discovery.
Devices reply to the discovery broadcast with a persistent device_id
regardless of their current IP, so this can find a device that has
moved to a new DHCP-assigned address.
"""
found = discover()
device = found.get(device_id)
return device[CONF_IP_ADDRESS] if device else None
async def async_setup_entry(hass: HomeAssistant, entry: MideaConfigEntry) -> bool:
"""Set up Midea from a config entry."""
data: Mapping[str, Any] = entry.data
device_id: int = data[CONF_DEVICE_ID]
ip_address: str = data[CONF_IP_ADDRESS]
device = await hass.async_add_executor_job(_create_device, data, ip_address)
if device is None:
raise ConfigEntryError("Unable to initialize device")
connected = await hass.async_add_executor_job(_connect, device)
if not connected:
new_ip_address = await hass.async_add_executor_job(
_discover_current_ip, device_id
)
if new_ip_address and new_ip_address != ip_address:
LOGGER.debug(
"Device %s moved from %s to %s, updating config entry",
device_id,
ip_address,
new_ip_address,
)
data = {**data, CONF_IP_ADDRESS: new_ip_address}
hass.config_entries.async_update_entry(entry, data=data)
new_device = await hass.async_add_executor_job(
_create_device, data, new_ip_address
)
if new_device is not None:
device = new_device
connected = await hass.async_add_executor_job(_connect, device)
if not connected:
raise ConfigEntryNotReady(f"Unable to connect to device {device_id}")
# The library's reconnect loop keeps retrying with a growing backoff
# (up to 600s) without checking for a stop request while sleeping, so
# device.close() alone cannot guarantee the background thread exits
# promptly when offline. Marking it a daemon thread ensures it can
# never block Home Assistant shutdown as a zombie thread.
device.daemon = True
await hass.async_add_executor_job(device.open)
entry.runtime_data = device
async def _close_device() -> None:
await hass.async_add_executor_job(device.close)
entry.async_on_unload(_close_device)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
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)