1 """Support for Xiaomi Miio."""
3 from __future__
import annotations
6 from collections.abc
import Callable, Coroutine
7 from dataclasses
import dataclass
8 from datetime
import timedelta
10 from typing
import Any
36 from miio.gateway.gateway
import GatewayException
65 MODELS_HUMIDIFIER_MIIO,
66 MODELS_HUMIDIFIER_MIOT,
67 MODELS_HUMIDIFIER_MJJSQ,
77 from .gateway
import ConnectXiaomiGateway
79 _LOGGER = logging.getLogger(__name__)
81 POLLING_TIMEOUT_SEC = 10
85 Platform.ALARM_CONTROL_PANEL,
90 SWITCH_PLATFORMS = [Platform.SWITCH]
92 Platform.BINARY_SENSOR,
100 HUMIDIFIER_PLATFORMS = [
101 Platform.BINARY_SENSOR,
108 LIGHT_PLATFORMS = [Platform.LIGHT]
110 Platform.BINARY_SENSOR,
115 AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR]
117 MODEL_TO_CLASS_MAP = {
119 MODEL_FAN_P9: FanMiot,
120 MODEL_FAN_P10: FanMiot,
121 MODEL_FAN_P11: FanMiot,
122 MODEL_FAN_P18: FanMiot,
124 MODEL_FAN_ZA5: FanZA5,
129 """Set up the Xiaomi Miio components from a config entry."""
130 hass.data.setdefault(DOMAIN, {})
131 if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
136 entry.data[CONF_FLOW_TYPE] != CONF_DEVICE
143 """Return the platforms belonging to a config_entry."""
144 model = config_entry.data[CONF_MODEL]
145 flow_type = config_entry.data[CONF_FLOW_TYPE]
147 if flow_type == CONF_GATEWAY:
148 return GATEWAY_PLATFORMS
149 if flow_type == CONF_DEVICE:
150 if model
in MODELS_SWITCH:
151 return SWITCH_PLATFORMS
152 if model
in MODELS_HUMIDIFIER:
153 return HUMIDIFIER_PLATFORMS
154 if model
in MODELS_FAN:
156 if model
in MODELS_LIGHT:
157 return LIGHT_PLATFORMS
158 for vacuum_model
in MODELS_VACUUM:
159 if model.startswith(vacuum_model):
160 return VACUUM_PLATFORMS
161 for air_monitor_model
in MODELS_AIR_MONITOR:
162 if model.startswith(air_monitor_model):
163 return AIR_MONITOR_PLATFORMS
166 "Unsupported device found! Please create an issue at "
167 "https://github.com/syssi/xiaomi_airpurifier/issues "
168 "and provide the following data: %s"
177 """Fetch data from the device using async_add_executor_job."""
179 async
def _async_fetch_data():
180 """Fetch data from the device."""
181 async
with asyncio.timeout(POLLING_TIMEOUT_SEC):
182 state = await hass.async_add_executor_job(device.status)
183 _LOGGER.debug(
"Got new state: %s", state)
187 return await _async_fetch_data()
188 except DeviceException
as ex:
189 if getattr(ex,
"code",
None) != -9999:
192 "Got exception while fetching the state, trying again: %s", ex
196 return await _async_fetch_data()
197 except DeviceException
as ex:
203 @dataclass(frozen=True)
205 """A class that holds the vacuum data retrieved by the coordinator."""
208 dnd_status: DNDStatus
209 last_clean_details: CleaningDetails
210 consumable_status: ConsumableStatus
211 clean_history_status: CleaningSummary
213 fan_speeds: dict[str, int]
214 fan_speeds_reverse: dict[int, str]
217 @dataclass(init=False, frozen=True)
219 """A class that holds attribute names for VacuumCoordinatorData.
221 These attributes can be used in methods like `getattr` when a generic solutions is
223 See homeassistant.components.xiaomi_miio.device.XiaomiCoordinatedMiioEntity
224 ._extract_value_from_attribute for
228 status: str =
"status"
229 dnd_status: str =
"dnd_status"
230 last_clean_details: str =
"last_clean_details"
231 consumable_status: str =
"consumable_status"
232 clean_history_status: str =
"clean_history_status"
234 fan_speeds: str =
"fan_speeds"
235 fan_speeds_reverse: str =
"fan_speeds_reverse"
239 hass: HomeAssistant, device: RoborockVacuum
240 ) -> Callable[[], Coroutine[Any, Any, VacuumCoordinatorData]]:
241 def update() -> VacuumCoordinatorData:
247 timer = device.timer()
248 except DeviceException
as ex:
250 "Unable to fetch timers, this may happen on some devices: %s", ex
253 fan_speeds = device.fan_speed_presets()
258 device.last_clean_details(),
259 device.consumable_status(),
260 device.clean_history(),
263 {v: k
for k, v
in fan_speeds.items()},
266 async
def update_async() -> VacuumCoordinatorData:
267 """Fetch data from the device using async_add_executor_job."""
269 async
def execute_update() -> VacuumCoordinatorData:
270 async
with asyncio.timeout(POLLING_TIMEOUT_SEC):
271 state = await hass.async_add_executor_job(update)
272 _LOGGER.debug(
"Got new vacuum state: %s", state)
276 return await execute_update()
277 except DeviceException
as ex:
278 if getattr(ex,
"code",
None) != -9999:
281 "Got exception while fetching the state, trying again: %s", ex
286 return await execute_update()
287 except DeviceException
as ex:
294 hass: HomeAssistant, entry: ConfigEntry
296 """Set up a data coordinator and one miio device to service multiple entities."""
297 model: str = entry.data[CONF_MODEL]
298 host = entry.data[CONF_HOST]
299 token = entry.data[CONF_TOKEN]
301 device: MiioDevice |
None =
None
303 update_method = _async_update_data_default
304 coordinator_class: type[DataUpdateCoordinator[Any]] = DataUpdateCoordinator
307 LAZY_DISCOVER_FOR_MODEL = {
308 "zhimi.fan.za3":
True,
309 "zhimi.fan.za5":
True,
310 "zhimi.airpurifier.za1":
True,
311 "dmaker.fan.1c":
True,
313 lazy_discover = LAZY_DISCOVER_FOR_MODEL.get(model,
False)
316 model
not in MODELS_HUMIDIFIER
317 and model
not in MODELS_FAN
318 and model
not in MODELS_VACUUM
319 and not model.startswith(ROBOROCK_GENERIC)
320 and not model.startswith(ROCKROBO_GENERIC)
324 _LOGGER.debug(
"Initializing with host %s (token %s...)", host, token[:5])
327 if model
in MODELS_HUMIDIFIER_MIOT:
328 device = AirHumidifierMiot(host, token, lazy_discover=lazy_discover)
330 elif model
in MODELS_HUMIDIFIER_MJJSQ:
331 device = AirHumidifierMjjsq(
332 host, token, lazy_discover=lazy_discover, model=model
335 elif model
in MODELS_HUMIDIFIER_MIIO:
336 device = AirHumidifier(host, token, lazy_discover=lazy_discover, model=model)
339 elif model
in MODELS_PURIFIER_MIOT:
340 device = AirPurifierMiot(host, token, lazy_discover=lazy_discover)
341 elif model.startswith(
"zhimi.airpurifier."):
342 device = AirPurifier(host, token, lazy_discover=lazy_discover)
343 elif model.startswith(
"zhimi.airfresh."):
344 device = AirFresh(host, token, lazy_discover=lazy_discover)
345 elif model == MODEL_AIRFRESH_A1:
346 device = AirFreshA1(host, token, lazy_discover=lazy_discover)
347 elif model == MODEL_AIRFRESH_T2017:
348 device = AirFreshT2017(host, token, lazy_discover=lazy_discover)
349 elif model
in MODELS_VACUUM
or model.startswith(
350 (ROBOROCK_GENERIC, ROCKROBO_GENERIC)
353 device = RoborockVacuum(host, token)
354 update_method = _async_update_data_vacuum
355 coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData]
357 elif model
in MODEL_TO_CLASS_MAP:
358 device = MODEL_TO_CLASS_MAP[model](host, token, lazy_discover=lazy_discover)
359 elif model
in MODELS_FAN_MIIO:
360 device = Fan(host, token, lazy_discover=lazy_discover, model=model)
364 "Unsupported device found! Please create an issue at "
365 "https://github.com/syssi/xiaomi_airpurifier/issues "
366 "and provide the following data: %s"
375 entity_registry = er.async_get(hass)
376 assert entry.unique_id
377 entity_id = entity_registry.async_get_entity_id(
"fan", DOMAIN, entry.unique_id)
381 if (entity := entity_registry.async_get(entity_id))
and (
382 migrate_entity_name := entity.name
384 hass.config_entries.async_update_entry(entry, title=migrate_entity_name)
385 entity_registry.async_remove(entity_id)
388 coordinator = coordinator_class(
393 update_method=update_method(hass, device),
395 update_interval=UPDATE_INTERVAL,
397 hass.data[DOMAIN][entry.entry_id] = {
399 KEY_COORDINATOR: coordinator,
403 await coordinator.async_config_entry_first_refresh()
407 """Set up the Xiaomi Gateway component from a config entry."""
408 host = entry.data[CONF_HOST]
409 token = entry.data[CONF_TOKEN]
411 gateway_id = entry.unique_id
418 await gateway.async_connect_gateway(host, token)
419 except AuthException
as error:
420 raise ConfigEntryAuthFailed
from error
421 except SetupException
as error:
422 raise ConfigEntryNotReady
from error
423 gateway_info = gateway.gateway_info
425 device_registry = dr.async_get(hass)
426 device_registry.async_get_or_create(
427 config_entry_id=entry.entry_id,
428 connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)},
429 identifiers={(DOMAIN, gateway_id)},
430 manufacturer=
"Xiaomi",
432 model=gateway_info.model,
433 sw_version=gateway_info.firmware_version,
434 hw_version=gateway_info.hardware_version,
437 def update_data_factory(sub_device):
438 """Create update function for a subdevice."""
440 async
def async_update_data():
441 """Fetch data from the subdevice."""
443 await hass.async_add_executor_job(sub_device.update)
444 except GatewayException
as ex:
445 _LOGGER.error(
"Got exception while fetching the state: %s", ex)
446 return {ATTR_AVAILABLE:
False}
447 return {ATTR_AVAILABLE:
True}
449 return async_update_data
451 coordinator_dict: dict[str, DataUpdateCoordinator] = {}
452 for sub_device
in gateway.gateway_device.devices.values():
459 update_method=update_data_factory(sub_device),
461 update_interval=UPDATE_INTERVAL,
464 hass.data[DOMAIN][entry.entry_id] = {
465 CONF_GATEWAY: gateway.gateway_device,
466 KEY_COORDINATOR: coordinator_dict,
469 await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS)
471 entry.async_on_unload(entry.add_update_listener(update_listener))
475 """Set up the Xiaomi Miio device component from a config entry."""
482 await hass.config_entries.async_forward_entry_setups(entry, platforms)
484 entry.async_on_unload(entry.add_update_listener(update_listener))
490 """Unload a config entry."""
493 unload_ok = await hass.config_entries.async_unload_platforms(
494 config_entry, platforms
498 hass.data[DOMAIN].pop(config_entry.entry_id)
504 """Handle options update."""
505 await hass.config_entries.async_reload(config_entry.entry_id)
IssData update(pyiss.ISS iss)
def get_platforms(config_entry)
def _async_update_data_default(hass, device)
Callable[[], Coroutine[Any, Any, VacuumCoordinatorData]] _async_update_data_vacuum(HomeAssistant hass, RoborockVacuum device)
bool async_setup_device_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
None update_listener(HomeAssistant hass, ConfigEntry config_entry)
None async_setup_gateway_entry(HomeAssistant hass, ConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)
None async_create_miio_device_and_coordinator(HomeAssistant hass, ConfigEntry entry)