This commit is contained in:
Home Assistant Version Control
2026-08-12 23:21:24 +00:00
parent 4517d1682f
commit f8ab9eda84
24 changed files with 625 additions and 468 deletions
+65 -65
View File
@@ -47,15 +47,14 @@ from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.json import save_json
from homeassistant.util.json import load_json
from midealocal.cloud import (
from midealan.cloud import (
PRESET_ACCOUNT_DATA,
SUPPORTED_CLOUDS,
MideaCloud,
get_midea_cloud,
)
from midealocal.device import AuthException, MideaDevice, ProtocolVersion
from midealocal.discover import discover
from midealocal.exceptions import SocketException
from midealan.device import MideaDevice, ProtocolVersion
from midealan.discover import discover
if TYPE_CHECKING:
from aiohttp import ClientSession
@@ -144,7 +143,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
record_file = storage_path.joinpath(f"{data[CONF_DEVICE_ID]!s}.json")
save_json(str(record_file), data)
def _load_device_config(self, device_id: str) -> Any: # ruff:ignore[any-type]
def _load_device_config(self, device_id: int | str) -> Any: # ruff:ignore[any-type]
"""Load device config from json file with device id.
Returns
@@ -356,7 +355,7 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
"""
# get all devices list
all_devices = discover()
all_devices = await self.hass.async_add_executor_job(discover)
# available devices exist
if len(all_devices) > 0:
table = (
@@ -402,8 +401,10 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
# ip exist
else:
ip_address = discovery_info[CONF_IP_ADDRESS]
# use midea-local discover() to get devices list with ip_address
self.devices = discover(list(self.supports.keys()), ip_address=ip_address)
# use midea-lan discover() to get devices list with ip_address
self.devices = await self.hass.async_add_executor_job(
lambda: discover(list(self.supports.keys()), ip_address=ip_address),
)
self.available_device = {}
for device_id, device in self.devices.items():
# remove exist devices and only return new devices
@@ -512,17 +513,8 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
subtype=0,
attributes={},
)
if dm.connect():
try:
dm.authenticate()
except AuthException:
_LOGGER.debug("Unable to authenticate.")
dm.close_socket()
except SocketException:
_LOGGER.debug("Socket closed.")
else:
dm.close_socket()
return value
if await self.hass.async_add_executor_job(self._try_connect_device, dm):
return value
# return debug log with failed key
_LOGGER.debug(
"connect device using method %s token/key failed",
@@ -533,6 +525,23 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
)
return {"error": "connect_error"}
@staticmethod
def _try_connect_device(dm: MideaDevice) -> bool:
"""Connect to a device, closing the socket afterwards.
Runs the blocking socket I/O in an executor so it never blocks the
event loop. V3 authentication is handled inside midea-lan's connect().
Returns
-------
True if the device connected successfully.
"""
try:
return dm.connect()
finally:
dm.close_socket()
async def async_step_auto(
self,
user_input: dict[str, Any] | None = None,
@@ -559,7 +568,10 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
CONF_PORT: device.get(CONF_PORT),
CONF_MODEL: device.get(CONF_MODEL),
}
storage_device = self._load_device_config(device_id)
storage_device = await self.hass.async_add_executor_job(
self._load_device_config,
device_id,
)
# device config already exist, load from local json without cloud
if self._check_storage_device(device, storage_device):
self.found_device = {
@@ -697,9 +709,8 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
if len(self.devices) < 1:
ip = user_input[CONF_IP_ADDRESS]
# discover device
self.devices = discover(
list(self.supports.keys()),
ip_address=ip,
self.devices = await self.hass.async_add_executor_job(
lambda: discover(list(self.supports.keys()), ip_address=ip),
)
# discover result MUST exist
if len(self.devices) != 1:
@@ -778,40 +789,31 @@ class MideaLanConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore[call-arg]
subtype=0,
attributes={},
)
if dm.connect():
try:
if user_input[CONF_PROTOCOL] == ProtocolVersion.V3:
dm.authenticate()
except SocketException:
_LOGGER.exception("Socket closed.")
except AuthException:
_LOGGER.exception(
"Unable to authenticate with provided key and token.",
)
dm.close_socket()
else:
dm.close_socket()
data = {
CONF_NAME: user_input[CONF_NAME],
CONF_DEVICE_ID: user_input[CONF_DEVICE_ID],
CONF_TYPE: user_input[CONF_TYPE],
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
CONF_MODEL: user_input[CONF_MODEL],
CONF_SUBTYPE: user_input[CONF_SUBTYPE],
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_KEY: user_input[CONF_KEY],
CONF_MAC: device.get(CONF_MAC),
CONF_SN: device.get(CONF_SN),
}
# save device json config when adding new device
self._save_device_config(data)
# finish add device entry
return self.async_create_entry(
title=f"{user_input[CONF_NAME]}",
data=data,
)
if await self.hass.async_add_executor_job(
self._try_connect_device,
dm,
):
data = {
CONF_NAME: user_input[CONF_NAME],
CONF_DEVICE_ID: user_input[CONF_DEVICE_ID],
CONF_TYPE: user_input[CONF_TYPE],
CONF_PROTOCOL: user_input[CONF_PROTOCOL],
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
CONF_PORT: user_input[CONF_PORT],
CONF_MODEL: user_input[CONF_MODEL],
CONF_SUBTYPE: user_input[CONF_SUBTYPE],
CONF_TOKEN: user_input[CONF_TOKEN],
CONF_KEY: user_input[CONF_KEY],
CONF_MAC: device.get(CONF_MAC),
CONF_SN: device.get(CONF_SN),
}
# save device json config when adding new device
await self.hass.async_add_executor_job(self._save_device_config, data)
# finish add device entry
return self.async_create_entry(
title=f"{user_input[CONF_NAME]}",
data=data,
)
return await self.async_step_manually(
error="Device auth failed with input config",
)
@@ -917,14 +919,12 @@ class MideaLanOptionsFlowHandler(OptionsFlow):
self._device_type = config_entry.data.get(CONF_TYPE)
if self._device_type is None:
self._device_type = 0xAC
if CONF_SENSORS in self._config_entry.options:
for key in self._config_entry.options[CONF_SENSORS]:
if key not in MIDEA_DEVICES[self._device_type]["entities"]:
self._config_entry.options[CONF_SENSORS].remove(key)
if CONF_SWITCHES in self._config_entry.options:
for key in self._config_entry.options[CONF_SWITCHES]:
if key not in MIDEA_DEVICES[self._device_type]["entities"]:
self._config_entry.options[CONF_SWITCHES].remove(key)
# Stale keys (attributes no longer in MIDEA_DEVICES) are filtered out
# downstream in async_step_init, where the multi-select defaults are
# computed as `set(sensors) & set(options)` / `set(switches) & ...` —
# both `sensors` and `switches` are built only from valid entities. No
# pruning is needed here; doing it in place mutated the list while
# iterating (skipping elements) and mutated HA-owned entry state.
async def async_step_init(
self,