From 08a0377dcb241262e26387449d525b93f090e4da Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 31 Aug 2021 16:44:13 +0200 Subject: [PATCH] Add support for Xiaomi Miio Air Purifier 3C (#55484) --- .../components/xiaomi_miio/__init__.py | 4 ++ homeassistant/components/xiaomi_miio/const.py | 33 ++++++++---- homeassistant/components/xiaomi_miio/fan.py | 52 +++++++++++++++++-- .../components/xiaomi_miio/number.py | 42 +++++++++++++++ .../components/xiaomi_miio/select.py | 3 ++ .../components/xiaomi_miio/sensor.py | 11 +++- .../components/xiaomi_miio/switch.py | 3 ++ 7 files changed, 131 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index cde597432df..de28ae78701 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -9,6 +9,7 @@ from miio import ( AirHumidifierMiot, AirHumidifierMjjsq, AirPurifier, + AirPurifierMB4, AirPurifierMiot, DeviceException, Fan, @@ -31,6 +32,7 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRPURIFIER_3C, MODEL_FAN_P5, MODELS_AIR_MONITOR, MODELS_FAN, @@ -139,6 +141,8 @@ async def async_create_miio_device_and_coordinator( device = AirHumidifier(host, token, model=model) migrate = True # Airpurifiers and Airfresh + elif model in MODEL_AIRPURIFIER_3C: + device = AirPurifierMB4(host, token) elif model in MODELS_PURIFIER_MIOT: device = AirPurifierMiot(host, token) elif model.startswith("zhimi.airpurifier."): diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index b670582c069..b63143c0f41 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -30,23 +30,24 @@ SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"] DEFAULT_CLOUD_COUNTRY = "cn" # Fan Models -MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" -MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" -MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3" -MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5" -MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6" -MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7" +MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" +MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" +MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" +MODEL_AIRPURIFIER_3C = "zhimi.airpurifier.mb4" +MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" MODEL_AIRPURIFIER_M1 = "zhimi.airpurifier.m1" MODEL_AIRPURIFIER_M2 = "zhimi.airpurifier.m2" MODEL_AIRPURIFIER_MA1 = "zhimi.airpurifier.ma1" MODEL_AIRPURIFIER_MA2 = "zhimi.airpurifier.ma2" +MODEL_AIRPURIFIER_PRO = "zhimi.airpurifier.v6" +MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1" +MODEL_AIRPURIFIER_PRO_V7 = "zhimi.airpurifier.v7" MODEL_AIRPURIFIER_SA1 = "zhimi.airpurifier.sa1" MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" -MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" -MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" -MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" -MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" -MODEL_AIRPURIFIER_PROH = "zhimi.airpurifier.va1" +MODEL_AIRPURIFIER_V1 = "zhimi.airpurifier.v1" +MODEL_AIRPURIFIER_V2 = "zhimi.airpurifier.v2" +MODEL_AIRPURIFIER_V3 = "zhimi.airpurifier.v3" +MODEL_AIRPURIFIER_V5 = "zhimi.airpurifier.v5" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" @@ -78,6 +79,7 @@ MODELS_FAN_MIIO = [ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, ] @@ -229,6 +231,8 @@ FEATURE_SET_CLEAN = 16384 FEATURE_SET_OSCILLATION_ANGLE = 32768 FEATURE_SET_OSCILLATION_ANGLE_MAX_140 = 65536 FEATURE_SET_DELAY_OFF_COUNTDOWN = 131072 +FEATURE_SET_LED_BRIGHTNESS_LEVEL = 262144 +FEATURE_SET_FAVORITE_RPM = 524288 FEATURE_FLAGS_AIRPURIFIER_MIIO = ( FEATURE_SET_BUZZER @@ -248,6 +252,13 @@ FEATURE_FLAGS_AIRPURIFIER_MIOT = ( | FEATURE_SET_LED_BRIGHTNESS ) +FEATURE_FLAGS_AIRPURIFIER_3C = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_LED_BRIGHTNESS_LEVEL + | FEATURE_SET_FAVORITE_RPM +) + FEATURE_FLAGS_AIRPURIFIER_PRO = ( FEATURE_SET_CHILD_LOCK | FEATURE_SET_LED diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 42828943d93..e62f7aa870c 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -34,6 +34,7 @@ from .const import ( DOMAIN, FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRPURIFIER_2S, + FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -47,6 +48,7 @@ from .const import ( KEY_DEVICE, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V3, @@ -193,7 +195,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - if model in MODELS_PURIFIER_MIOT: + if model == MODEL_AIRPURIFIER_3C: + entity = XiaomiAirPurifierMB4( + name, + device, + config_entry, + unique_id, + coordinator, + ) + elif model in MODELS_PURIFIER_MIOT: entity = XiaomiAirPurifierMiot( name, device, @@ -567,11 +577,35 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): self._fan_level = fan_level self.async_write_ha_state() - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode of the fan. - This method is a coroutine. - """ +class XiaomiAirPurifierMB4(XiaomiGenericDevice): + """Representation of a Xiaomi Air Purifier MB4.""" + + PRESET_MODE_MAPPING = { + "Auto": AirpurifierMiotOperationMode.Auto, + "Silent": AirpurifierMiotOperationMode.Silent, + "Favorite": AirpurifierMiotOperationMode.Favorite, + } + + def __init__(self, name, device, entry, unique_id, coordinator): + """Initialize Air Purifier MB4.""" + super().__init__(name, device, entry, unique_id, coordinator) + + self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C + self._preset_modes = list(self.PRESET_MODE_MAPPING) + self._supported_features = SUPPORT_PRESET_MODE + + @property + def preset_mode(self): + """Get the active preset mode.""" + if self.coordinator.data.is_on: + preset_mode = AirpurifierMiotOperationMode(self._mode).name + return preset_mode if preset_mode in self._preset_modes else None + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" if preset_mode not in self.preset_modes: _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) return @@ -583,6 +617,14 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): self._mode = self.PRESET_MODE_MAPPING[preset_mode].value self.async_write_ha_state() + @callback + def _handle_coordinator_update(self): + """Fetch state from the device.""" + self._available = True + self._state = self.coordinator.data.is_on + self._mode = self.coordinator.data.mode.value + self.async_write_ha_state() + class XiaomiAirFresh(XiaomiGenericDevice): """Representation of a Xiaomi Air Fresh.""" diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index a31478df1f3..2547c33bbfa 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -17,6 +17,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_CA4, FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRPURIFIER_2S, + FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -28,6 +29,8 @@ from .const import ( FEATURE_SET_DELAY_OFF_COUNTDOWN, FEATURE_SET_FAN_LEVEL, FEATURE_SET_FAVORITE_LEVEL, + FEATURE_SET_FAVORITE_RPM, + FEATURE_SET_LED_BRIGHTNESS_LEVEL, FEATURE_SET_MOTOR_SPEED, FEATURE_SET_OSCILLATION_ANGLE, FEATURE_SET_OSCILLATION_ANGLE_MAX_140, @@ -39,6 +42,7 @@ from .const import ( MODEL_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -58,6 +62,8 @@ from .device import XiaomiCoordinatedMiioEntity ATTR_DELAY_OFF_COUNTDOWN = "delay_off_countdown" ATTR_FAN_LEVEL = "fan_level" ATTR_FAVORITE_LEVEL = "favorite_level" +ATTR_FAVORITE_RPM = "favorite_rpm" +ATTR_LED_BRIGHTNESS_LEVEL = "led_brightness_level" ATTR_MOTOR_SPEED = "motor_speed" ATTR_OSCILLATION_ANGLE = "angle" ATTR_VOLUME = "volume" @@ -143,6 +149,25 @@ NUMBER_TYPES = { step=1, method="async_set_delay_off_countdown", ), + FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( + key=ATTR_LED_BRIGHTNESS_LEVEL, + name="Led Brightness", + icon="mdi:brightness-6", + min_value=0, + max_value=8, + step=1, + method="async_set_led_brightness_level", + ), + FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( + key=ATTR_FAVORITE_RPM, + name="Favorite Motor Speed", + icon="mdi:star-cog", + unit_of_measurement="rpm", + min_value=300, + max_value=2300, + step=10, + method="async_set_favorite_rpm", + ), } MODEL_TO_FEATURES_MAP = { @@ -151,6 +176,7 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRHUMIDIFIER_CA4: FEATURE_FLAGS_AIRHUMIDIFIER_CA4, MODEL_AIRHUMIDIFIER_CB1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRPURIFIER_2S: FEATURE_FLAGS_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_3C: FEATURE_FLAGS_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO: FEATURE_FLAGS_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, @@ -293,3 +319,19 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): self._device.delay_off, delay_off_countdown * 60, ) + + async def async_set_led_brightness_level(self, level: int): + """Set the led brightness level.""" + return await self._try_command( + "Setting the led brightness level of the miio device failed.", + self._device.set_led_brightness_level, + level, + ) + + async def async_set_favorite_rpm(self, rpm: int): + """Set the target motor speed.""" + return await self._try_command( + "Setting the favorite rpm of the miio device failed.", + self._device.set_favorite_rpm, + rpm, + ) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index b43291dfeef..daa721fef95 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -23,6 +23,7 @@ from .const import ( KEY_COORDINATOR, KEY_DEVICE, MODEL_AIRFRESH_VA2, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2, MODEL_FAN_SA1, @@ -75,6 +76,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): model = config_entry.data[CONF_MODEL] device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + if model == MODEL_AIRPURIFIER_3C: + return if model in MODELS_HUMIDIFIER_MIIO: entity_class = XiaomiAirHumidifierSelector elif model in MODELS_HUMIDIFIER_MIOT: diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 63535e88a2d..96bcdf9145d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -33,6 +33,7 @@ from homeassistant.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -56,6 +57,7 @@ from .const import ( MODEL_AIRFRESH_VA2, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V2, @@ -194,7 +196,7 @@ SENSOR_TYPES = { key=ATTR_AQI, name="PM2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:blur", + device_class=DEVICE_CLASS_PM25, state_class=STATE_CLASS_MEASUREMENT, ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( @@ -268,6 +270,12 @@ PURIFIER_MIOT_SENSORS = ( ATTR_PURIFY_VOLUME, ATTR_TEMPERATURE, ) +PURIFIER_3C_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_USE, + ATTR_MOTOR_SPEED, + ATTR_PM25, +) PURIFIER_V2_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, @@ -326,6 +334,7 @@ MODEL_TO_SENSORS_MAP = { MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, + MODEL_AIRPURIFIER_3C: PURIFIER_3C_SENSORS, MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index c40711f5266..7859ff75ec6 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -38,6 +38,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRHUMIDIFIER_MJSSQ, FEATURE_FLAGS_AIRPURIFIER_2S, + FEATURE_FLAGS_AIRPURIFIER_3C, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -61,6 +62,7 @@ from .const import ( MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -158,6 +160,7 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRHUMIDIFIER_CB1: FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, MODEL_AIRPURIFIER_2H: FEATURE_FLAGS_AIRPURIFIER_2S, MODEL_AIRPURIFIER_2S: FEATURE_FLAGS_AIRPURIFIER_2S, + MODEL_AIRPURIFIER_3C: FEATURE_FLAGS_AIRPURIFIER_3C, MODEL_AIRPURIFIER_PRO: FEATURE_FLAGS_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1,