diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 4663813ab7c..476132aa8b2 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,4 +1,6 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" +from __future__ import annotations + from dataclasses import dataclass from enum import Enum import logging @@ -17,6 +19,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( @@ -45,6 +48,7 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, ) @@ -64,79 +68,104 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -ATTR_POWER = "power" +ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" +ATTR_AIR_QUALITY = "air_quality" ATTR_CHARGING = "charging" ATTR_DISPLAY_CLOCK = "display_clock" +ATTR_HUMIDITY = "humidity" +ATTR_ILLUMINANCE = "illuminance" +ATTR_LOAD_POWER = "load_power" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" +ATTR_POWER = "power" +ATTR_PRESSURE = "pressure" ATTR_SENSOR_STATE = "sensor_state" ATTR_WATER_LEVEL = "water_level" -ATTR_HUMIDITY = "humidity" -ATTR_ACTUAL_MOTOR_SPEED = "actual_speed" @dataclass -class SensorType: +class XiaomiMiioSensorDescription(SensorEntityDescription): """Class that holds device specific info for a xiaomi aqara or humidifier sensor.""" - unit: str = None - icon: str = None - device_class: str = None - state_class: str = None - valid_min_value: float = None - valid_max_value: float = None + valid_min_value: float | None = None + valid_max_value: float | None = None SENSOR_TYPES = { - "temperature": SensorType( - unit=TEMP_CELSIUS, + ATTR_TEMPERATURE: XiaomiMiioSensorDescription( + key=ATTR_TEMPERATURE, + name="Temperature", + unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), - "humidity": SensorType( - unit=PERCENTAGE, + ATTR_HUMIDITY: XiaomiMiioSensorDescription( + key=ATTR_HUMIDITY, + name="Humidity", + unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - "pressure": SensorType( - unit=PRESSURE_HPA, + ATTR_PRESSURE: XiaomiMiioSensorDescription( + key=ATTR_PRESSURE, + name="Pressure", + unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, ), - "load_power": SensorType( - unit=POWER_WATT, + ATTR_LOAD_POWER: XiaomiMiioSensorDescription( + key=ATTR_LOAD_POWER, + name="Load Power", + unit_of_measurement=POWER_WATT, device_class=DEVICE_CLASS_POWER, ), - "water_level": SensorType( - unit=PERCENTAGE, + ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( + key=ATTR_WATER_LEVEL, + name="Water Level", + unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=STATE_CLASS_MEASUREMENT, valid_min_value=0.0, valid_max_value=100.0, ), - "actual_speed": SensorType( - unit="rpm", + ATTR_ACTUAL_MOTOR_SPEED: XiaomiMiioSensorDescription( + key=ATTR_ACTUAL_MOTOR_SPEED, + name="Actual Speed", + unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, valid_min_value=200.0, valid_max_value=2000.0, ), + ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( + key=ATTR_ILLUMINANCE, + name="Illuminance", + unit_of_measurement=UNIT_LUMEN, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + ATTR_AIR_QUALITY: XiaomiMiioSensorDescription( + key=ATTR_AIR_QUALITY, + unit_of_measurement="AQI", + icon="mdi:cloud", + state_class=STATE_CLASS_MEASUREMENT, + ), } -HUMIDIFIER_SENSORS = { +HUMIDIFIER_MIIO_SENSORS = { ATTR_HUMIDITY: "humidity", ATTR_TEMPERATURE: "temperature", } -HUMIDIFIER_SENSORS_MIOT = { +HUMIDIFIER_MIOT_SENSORS = { ATTR_HUMIDITY: "humidity", ATTR_TEMPERATURE: "temperature", ATTR_WATER_LEVEL: "water_level", ATTR_ACTUAL_MOTOR_SPEED: "actual_speed", } -HUMIDIFIER_SENSORS_MJJSQ = { +HUMIDIFIER_MJJSQ_SENSORS = { ATTR_HUMIDITY: "humidity", ATTR_TEMPERATURE: "temperature", } @@ -170,24 +199,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): GATEWAY_MODEL_AC_V3, GATEWAY_MODEL_EU, ]: + description = SENSOR_TYPES[ATTR_ILLUMINANCE] entities.append( XiaomiGatewayIlluminanceSensor( - gateway, config_entry.title, config_entry.unique_id + gateway, config_entry.title, config_entry.unique_id, description ) ) # Gateway sub devices sub_devices = gateway.devices coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): - sensor_variables = set(sub_device.status) & set(SENSOR_TYPES) - if sensor_variables: - entities.extend( - [ - XiaomiGatewaySensor( - coordinator, sub_device, config_entry, variable - ) - for variable in sensor_variables - ] + for sensor, description in SENSOR_TYPES.items(): + if sensor not in sub_device.status: + continue + entities.append( + XiaomiGatewaySensor( + coordinator, sub_device, config_entry, description + ) ) elif config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: host = config_entry.data[CONF_HOST] @@ -197,31 +225,36 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] if model in MODELS_HUMIDIFIER_MIOT: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = HUMIDIFIER_SENSORS_MIOT + sensors = HUMIDIFIER_MIOT_SENSORS elif model in MODELS_HUMIDIFIER_MJJSQ: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = HUMIDIFIER_SENSORS_MJJSQ - elif model.startswith("zhimi.humidifier."): + sensors = HUMIDIFIER_MJJSQ_SENSORS + elif model in MODELS_HUMIDIFIER_MIIO: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = HUMIDIFIER_SENSORS + sensors = HUMIDIFIER_MIIO_SENSORS else: unique_id = config_entry.unique_id name = config_entry.title _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) device = AirQualityMonitor(host, token) + description = SENSOR_TYPES[ATTR_AIR_QUALITY] entities.append( - XiaomiAirQualityMonitor(name, device, config_entry, unique_id) + XiaomiAirQualityMonitor( + name, device, config_entry, unique_id, description + ) ) - for sensor in sensors: + for sensor, description in SENSOR_TYPES.items(): + if sensor not in sensors: + continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {sensor.replace('_', ' ').title()}", + f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", - sensor, hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, ) ) @@ -231,34 +264,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): """Representation of a Xiaomi Humidifier sensor.""" - def __init__(self, name, device, entry, unique_id, attribute, coordinator): + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) - self._sensor_config = SENSOR_TYPES[attribute] - self._attr_device_class = self._sensor_config.device_class - self._attr_state_class = self._sensor_config.state_class - self._attr_icon = self._sensor_config.icon self._attr_name = name self._attr_unique_id = unique_id - self._attr_unit_of_measurement = self._sensor_config.unit - self._device = device - self._entry = entry - self._attribute = attribute self._state = None + self.entity_description = description @property def state(self): """Return the state of the device.""" self._state = self._extract_value_from_attribute( - self.coordinator.data, self._attribute + self.coordinator.data, self.entity_description.key ) if ( - self._sensor_config.valid_min_value - and self._state < self._sensor_config.valid_min_value + self.entity_description.valid_min_value + and self._state < self.entity_description.valid_min_value ) or ( - self._sensor_config.valid_max_value - and self._state > self._sensor_config.valid_max_value + self.entity_description.valid_max_value + and self._state > self.entity_description.valid_max_value ): return None return self._state @@ -275,12 +301,10 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, description): """Initialize the entity.""" super().__init__(name, device, entry, unique_id) - self._icon = "mdi:cloud" - self._unit_of_measurement = "AQI" self._available = None self._state = None self._state_attrs = { @@ -293,16 +317,7 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): ATTR_NIGHT_TIME_END: None, ATTR_SENSOR_STATE: None, } - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return self._icon + self.entity_description = description @property def available(self): @@ -349,69 +364,33 @@ class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Representation of a XiaomiGatewaySensor.""" - def __init__(self, coordinator, sub_device, entry, data_key): + def __init__(self, coordinator, sub_device, entry, description): """Initialize the XiaomiSensor.""" super().__init__(coordinator, sub_device, entry) - self._data_key = data_key - self._unique_id = f"{sub_device.sid}-{data_key}" - self._name = f"{data_key} ({sub_device.sid})".capitalize() - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return SENSOR_TYPES[self._data_key].icon - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self._data_key].unit - - @property - def device_class(self): - """Return the device class of this entity.""" - return SENSOR_TYPES[self._data_key].device_class - - @property - def state_class(self): - """Return the state class of this entity.""" - return SENSOR_TYPES[self._data_key].state_class + self._unique_id = f"{sub_device.sid}-{description.key}" + self._name = f"{description.key} ({sub_device.sid})".capitalize() + self.entity_description = description @property def state(self): """Return the state of the sensor.""" - return self._sub_device.status[self._data_key] + return self._sub_device.status[self.entity_description.key] class XiaomiGatewayIlluminanceSensor(SensorEntity): """Representation of the gateway device's illuminance sensor.""" - _attr_device_class = DEVICE_CLASS_ILLUMINANCE - _attr_unit_of_measurement = UNIT_LUMEN - - def __init__(self, gateway_device, gateway_name, gateway_device_id): + def __init__(self, gateway_device, gateway_name, gateway_device_id, description): """Initialize the entity.""" + + self._attr_name = f"{gateway_name} {description.name}" + self._attr_unique_id = f"{gateway_device_id}-{description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, gateway_device_id)}} self._gateway = gateway_device - self._name = f"{gateway_name} Illuminance" - self._gateway_device_id = gateway_device_id - self._unique_id = f"{gateway_device_id}-illuminance" + self.entity_description = description self._available = False self._state = None - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info of the gateway.""" - return {"identifiers": {(DOMAIN, self._gateway_device_id)}} - - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def available(self): """Return true when state is known."""