From 106e930aad0d6ab43b7e71b9bb57d6fe7a01af1a Mon Sep 17 00:00:00 2001 From: Dennis Date: Sun, 25 Apr 2021 13:43:47 +0300 Subject: [PATCH 1/9] zhimi.humidifier.ca1: Add example configuration (#179) --- README.md | 2 +- docs/zhimi.humidifier.ca1.yaml | 183 +++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 docs/zhimi.humidifier.ca1.yaml diff --git a/README.md b/README.md index 9ef429a..5178e56 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ This custom component is more or less the beta version of the [official componen | Air Purifier 3 (2019) | zhimi.airpurifier.ma4 | | | | Air Purifier 3H (2019) | zhimi.airpurifier.mb3 | FJY4031GL(?), XM200017 | 45m2, 380m3/h CADR, 64dB, 38W (max) | | Air Humidifier | zhimi.humidifier.v1 | | | -| Air Humidifier CA1 | zhimi.humidifier.ca1 | | | +| Air Humidifier CA1 | [zhimi.humidifier.ca1](docs/zhimi.humidifier.ca1.yaml) | | | | Smartmi Humidifier Evaporator 2 | zhimi.humidifier.ca4 | CJXJSQ04ZM | | | Smartmi Evaporative Humidifier | zhimi.humidifier.cb1 | CJXJSQ02ZM, SKV6001EU | 8W, 240x240x363mm | | Mijia Smart Sterilization Humidifier S | deerma.humidifier.mjjsq | MJJSQ03DY | 4.5L, <=39dB, 450mL/h, 40W | diff --git a/docs/zhimi.humidifier.ca1.yaml b/docs/zhimi.humidifier.ca1.yaml new file mode 100644 index 0000000..71128e5 --- /dev/null +++ b/docs/zhimi.humidifier.ca1.yaml @@ -0,0 +1,183 @@ +fan humidifier: + - platform: xiaomi_miio_airpurifier + name: Humidifier + host: + token: + model: zhimi.humidifier.ca1 + +switch: + - platform: template + switches: + humidifier_buzzer: + friendly_name: Buzzer + value_template: "{{ is_state_attr('fan.humidifier', 'buzzer', True) }}" + turn_on: + service: xiaomi_miio_airpurifier.fan_set_buzzer_on + data: + entity_id: fan.humidifier + turn_off: + service: xiaomi_miio_airpurifier.fan_set_buzzer_off + data: + entity_id: fan.humidifier + icon_template: mdi:volume-high + + humidifier_lock: + friendly_name: Child lock + value_template: "{{ is_state_attr('fan.humidifier', 'child_lock', True) }}" + turn_on: + service: xiaomi_miio_airpurifier.fan_set_child_lock_on + data: + entity_id: fan.humidifier + turn_off: + service: xiaomi_miio_airpurifier.fan_set_child_lock_off + data: + entity_id: fan.humidifier + icon_template: mdi:lock-outline + + humidifier_dry_mode: + friendly_name: Dry mode + value_template: "{{ is_state_attr('fan.humidifier', 'dry', True) }}" + turn_on: + service: xiaomi_miio_airpurifier.fan_set_dry_on + data: + entity_id: fan.humidifier + turn_off: + service: xiaomi_miio_airpurifier.fan_set_dry_off + data: + entity_id: fan.humidifier + icon_template: mdi:hair-dryer-outline + +sensor: + - platform: template + sensors: + humidifier_water_level: + friendly_name: Water level + value_template: '{{ (states.fan.humidifier.attributes.depth / 125 * 100) | round() }}' # provides value in [0, ..., 125] + unit_of_measurement: '%' + icon_template: mdi:waves + + humidifier_humidity: + friendly_name: Humidity + value_template: '{{ states.fan.humidifier.attributes.humidity }}' + unit_of_measurement: '%' + icon_template: mdi:water-percent + + humidifier_temperature: + friendly_name: Temperature + value_template: '{{ states.fan.humidifier.attributes.temperature }}' + unit_of_measurement: '°C' + icon_template: mdi:thermometer + +input_select: + humidifier_mode: + name: Operation mode + options: + - Auto + - Silent + - Medium + - High + + humidifier_led: + name: Led mode + options: + - 'Off' # will be converted to False without quotes + - Dim + - Bright + + humidifier_target_humidity: + name: Target humidity + options: + - 30 + - 40 + - 50 + - 60 + - 70 + - 80 + +automation: + - alias: Humidifier - select operation mode by input select + trigger: + entity_id: input_select.humidifier_mode + platform: state + action: + service: fan.set_preset_mode + data_template: + entity_id: fan.humidifier + preset_mode: "{{ states('input_select.humidifier_mode') }}" + + - alias: Humidifier - monitor operation mode and update input select + trigger: + platform: template + value_template: '{{ states.fan.humidifier.attributes.preset_mode }}' + action: + service: input_select.select_option + entity_id: input_select.humidifier_mode + data_template: + option: > + {{ states.fan.humidifier.attributes.preset_mode }} + + - alias: Humidifier - select LED mode by input select + trigger: + entity_id: input_select.humidifier_led + platform: state + action: + service: xiaomi_miio_airpurifier.fan_set_led_brightness + data_template: + entity_id: fan.humidifier + brightness: > # accepts value in [0, 1, 2], input provides value in [Off, Dim, Bright] + {% set str_to_int = + { 'Off':'2', + 'Dim':'1', + 'Bright':'0' } %} + {% set str_brightness = states('input_select.humidifier_led') %} + {% set int_brightness = str_to_int[str_brightness] if str_brightness in str_to_int %} + {{ int_brightness }} + + - alias: Humidifier - monitor LED mode and update input select + trigger: + platform: template + value_template: '{{ states.fan.humidifier.attributes.led_brightness }}' + action: + service: input_select.select_option + entity_id: input_select.humidifier_led + data_template: + option: > # provides value in [0, 1, 2], input accepts value in [Off, Dim, Bright] + {% set int_to_str = + { 2:'Off', + 1:'Dim', + 0:'Bright' } %} + {% set int_brightness = states.fan.humidifier.attributes.led_brightness %} + {% set str_brightness = int_to_str[int_brightness] if int_brightness in int_to_str %} + {{ str_brightness }} + + - alias: Humidifier - select target humidity by input select + trigger: + platform: state + entity_id: input_select.humidifier_target_humidity + action: + service: xiaomi_miio_airpurifier.fan_set_target_humidity + data_template: + entity_id: fan.humidifier + humidity: > + {{ states('input_select.humidifier_target_humidity') }} + + - alias: Humidifier - monitor target humidity and update input select + trigger: + platform: template + value_template: '{{ states.fan.humidifier.attributes.target_humidity }}' + action: + service: input_select.select_option + entity_id: input_select.humidifier_target_humidity + data_template: + option: > + {{ states.fan.humidifier.attributes.target_humidity }} + +# automation humidifier_water_level_low: +# trigger: +# - platform: numeric_state +# entity_id: sensor.humidifier_water_level +# below: 30 +# action: +# - service: notify. +# data_template: +# message: "Humidifier water level low - {{ states('sensor.humidifier_water_level') }}%" From 676e0407d40c493a59be80617c2830cbd2f16a62 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Apr 2021 20:24:17 +0200 Subject: [PATCH 2/9] Add iot class --- custom_components/xiaomi_miio_airpurifier/fan.py | 2 +- custom_components/xiaomi_miio_airpurifier/manifest.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index 892bc94..bb24bf0 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -1292,7 +1292,7 @@ def __init__(self, name, device, model, unique_id, retries=0): self._device_features = FEATURE_FLAGS_AIRPURIFIER_2H self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2H self._preset_modes = OPERATION_MODES_AIRPURIFIER_2H - elif self._model == MODEL_AIRPURIFIER_3 or self._model == MODEL_AIRPURIFIER_3H: + elif self._model in PURIFIER_MIOT: self._device_features = FEATURE_FLAGS_AIRPURIFIER_3 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_3 self._preset_modes = OPERATION_MODES_AIRPURIFIER_3 diff --git a/custom_components/xiaomi_miio_airpurifier/manifest.json b/custom_components/xiaomi_miio_airpurifier/manifest.json index 12ac996..f1663e4 100644 --- a/custom_components/xiaomi_miio_airpurifier/manifest.json +++ b/custom_components/xiaomi_miio_airpurifier/manifest.json @@ -2,6 +2,7 @@ "domain": "xiaomi_miio_airpurifier", "name": "Xiaomi Mi Air Purifier, Air Humidifier, Air Fresh and Pedestal Fan Integration", "version": "0.6.9", + "iot_class": "local_polling", "config_flow": false, "documentation": "https://github.com/syssi/xiaomi_airpurifier", "issue_tracker": "https://github.com/syssi/xiaomi_airpurifier/issues", From fb3e5e1d8a1bdab08514410fd430028543179cd0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 6 May 2021 22:16:15 +0200 Subject: [PATCH 3/9] Add dmaker.fan.1c support (#166) --- README.md | 1 + .../xiaomi_miio_airpurifier/fan.py | 166 ++++++++++++++++++ .../xiaomi_miio_airpurifier/manifest.json | 2 +- 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5178e56..89c7ac2 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ This custom component is more or less the beta version of the [official componen | Pedestal Fan Fan ZA1 | zhimi.fan.za1 | | | | Pedestal Fan Fan ZA3 | zhimi.fan.za3 | | | | Pedestal Fan Fan ZA4 | zhimi.fan.za4 | | | +| Pedestal Fan Fan 1C | dmaker.fan.1c | | | | Pedestal Fan Fan P5 | dmaker.fan.p5 | | | | Pedestal Fan Fan P9 | dmaker.fan.p9 | | | | Pedestal Fan Fan P10 | dmaker.fan.p10 | | | diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index bb24bf0..15b90d2 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -19,6 +19,7 @@ Fan, FanLeshow, FanP5, + FanC1, FanP9, FanP10, FanP11, @@ -64,6 +65,9 @@ from miio.fan_leshow import ( # pylint: disable=import-error, import-error OperationMode as FanLeshowOperationMode, ) +from miio.fan_miot import ( + OperationModeMiot as FanOperationModeMiot +) import voluptuous as vol from homeassistant.components.fan import ( @@ -138,6 +142,7 @@ MODEL_FAN_P10 = "dmaker.fan.p10" MODEL_FAN_P11 = "dmaker.fan.p11" MODEL_FAN_LESHOW_SS4 = "leshow.fan.ss4" +MODEL_FAN_1C = "dmaker.fan.1c" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -185,6 +190,7 @@ MODEL_FAN_P10, MODEL_FAN_P11, MODEL_FAN_LESHOW_SS4, + MODEL_FAN_1C, ] ), vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, @@ -581,6 +587,16 @@ ATTR_ERROR_DETECTED: "error_detected", } +AVAILABLE_ATTRIBUTES_FAN_1C = { + ATTR_MODE: "mode", + ATTR_RAW_SPEED: "speed", + ATTR_BUZZER: "buzzer", + ATTR_OSCILLATE: "oscillate", + ATTR_DELAY_OFF_COUNTDOWN: "delay_off_countdown", + ATTR_LED: "led", + ATTR_CHILD_LOCK: "child_lock", +} + FAN_SPEED_LEVEL1 = "Level 1" FAN_SPEED_LEVEL2 = "Level 2" FAN_SPEED_LEVEL3 = "Level 3" @@ -610,6 +626,13 @@ FAN_SPEED_LEVEL4: 100, } +FAN_PRESET_MODES_1C = { + SPEED_OFF: 0, + FAN_SPEED_LEVEL1: 1, + FAN_SPEED_LEVEL2: 2, + FAN_SPEED_LEVEL3: 3, +} + OPERATION_MODES_AIRPURIFIER = ["Auto", "Silent", "Favorite", "Idle"] OPERATION_MODES_AIRPURIFIER_PRO = ["Auto", "Silent", "Favorite"] OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO @@ -805,6 +828,7 @@ ) FEATURE_FLAGS_FAN_LESHOW_SS4 = FEATURE_SET_BUZZER +FEATURE_FLAGS_FAN_1C = FEATURE_FLAGS_FAN SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" @@ -1071,6 +1095,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= elif model == MODEL_FAN_LESHOW_SS4: fan = FanLeshow(host, token, model=model) device = XiaomiFanLeshow(name, fan, model, unique_id, retries) + elif model == MODEL_FAN_1C: + fan = FanC1(host, token, model=model) + device = XiaomiFan1C(name, fan, model, unique_id, retries) else: _LOGGER.error( "Unsupported device found! Please create an issue at " @@ -2639,3 +2666,142 @@ async def async_set_delay_off(self, delay_off_countdown: int) -> None: self._device.delay_off, delay_off_countdown, ) + + +class XiaomiFan1C(XiaomiFan): + """Representation of a Xiaomi Fan 1C.""" + + def __init__(self, name, device, model, unique_id, retries): + """Initialize the fan entity.""" + super().__init__(name, device, model, unique_id, retries) + + self._device_features = FEATURE_FLAGS_FAN_1C + self._available_attributes = AVAILABLE_ATTRIBUTES_FAN_1C + self._preset_modes = FAN_PRESET_MODES_1C + self._oscillate = None + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes} + ) + + @property + def supported_features(self) -> int: + """Supported features.""" + return SUPPORT_PRESET_MODE | SUPPORT_OSCILLATE + + async def async_update(self): + """Fetch state from the device.""" + # On state change the device doesn't provide the new state immediately. + if self._skip_update: + self._skip_update = False + return + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._oscillate = state.oscillate + self._state = state.is_on + + for preset_mode, value in FAN_PRESET_MODES_1C.items(): + if state.speed == value: + self._preset_mode = preset_mode + + self._state_attrs.update( + { + key: self._extract_value_from_attribute(state, value) + for key, value in self._available_attributes.items() + } + ) + self._retry = 0 + + except DeviceException as ex: + self._retry = self._retry + 1 + if self._retry < self._retries: + _LOGGER.info( + "Got exception while fetching the state: %s , _retry=%s", + ex, + self._retry, + ) + else: + self._available = False + _LOGGER.error( + "Got exception while fetching the state: %s , _retry=%s", + ex, + self._retry, + ) + + @property + def preset_modes(self): + """Get the list of available preset modes.""" + return self._preset_modes + + @property + def preset_mode(self): + """Get the current preset mode.""" + if self._state: + return self._preset_mode + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + _LOGGER.debug("Setting the preset mode to: %s", preset_mode) + + await self._try_command( + "Setting preset mode of the miio device failed.", + self._device.set_speed, + FAN_PRESET_MODES_1C[preset_mode], + ) + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._oscillate + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation.""" + if oscillating: + await self._try_command( + "Setting oscillate on of the miio device failed.", + self._device.set_oscillate, + True, + ) + else: + await self._try_command( + "Setting oscillate off of the miio device failed.", + self._device.set_oscillate, + False, + ) + + async def async_set_delay_off(self, delay_off_countdown: int) -> None: + """Set scheduled off timer in minutes.""" + + await self._try_command( + "Setting delay off miio device failed.", + self._device.delay_off, + delay_off_countdown, + ) + + async def async_set_natural_mode_on(self): + """Turn the natural mode on.""" + if self._device_features & FEATURE_SET_NATURAL_MODE == 0: + return + + await self._try_command( + "Setting fan natural mode of the miio device failed.", + self._device.set_mode, + FanOperationModeMiot.Nature, + ) + + async def async_set_natural_mode_off(self): + """Turn the natural mode off.""" + if self._device_features & FEATURE_SET_NATURAL_MODE == 0: + return + + await self._try_command( + "Setting fan natural mode of the miio device failed.", + self._device.set_mode, + FanOperationModeMiot.Normal, + ) diff --git a/custom_components/xiaomi_miio_airpurifier/manifest.json b/custom_components/xiaomi_miio_airpurifier/manifest.json index f1663e4..0b27439 100644 --- a/custom_components/xiaomi_miio_airpurifier/manifest.json +++ b/custom_components/xiaomi_miio_airpurifier/manifest.json @@ -8,7 +8,7 @@ "issue_tracker": "https://github.com/syssi/xiaomi_airpurifier/issues", "requirements": [ "construct==2.10.56", - "python-miio>=0.5.5" + "python-miio>=0.5.6" ], "dependencies": [], "codeowners": [ From c74a5b58401c1efae23ad32c6a8080bd64eeae45 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 6 May 2021 22:27:20 +0200 Subject: [PATCH 4/9] Bump minimum required homeassistant version --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index b22692f..c907df9 100644 --- a/hacs.json +++ b/hacs.json @@ -3,5 +3,5 @@ "content_in_root": false, "render_readme": true, "iot_class": "local_polling", - "homeassistant": "2021.4.0" + "homeassistant": "2021.5.1" } From 30666a1ac19daaba88cd2e768b0668ddac5331ce Mon Sep 17 00:00:00 2001 From: alexeypetrenko Date: Fri, 7 May 2021 01:28:15 +0500 Subject: [PATCH 5/9] AirDogX3, AirDogX5, AirDogX7SM support (#159) --- .../xiaomi_miio_airpurifier/fan.py | 254 ++++++++++++++++++ .../xiaomi_miio_airpurifier/services.yaml | 9 + 2 files changed, 263 insertions(+) diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index 15b90d2..0283cc1 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -19,6 +19,9 @@ Fan, FanLeshow, FanP5, + AirDogX3, + AirDogX5, + AirDogX7SM, FanC1, FanP9, FanP10, @@ -65,6 +68,9 @@ from miio.fan_leshow import ( # pylint: disable=import-error, import-error OperationMode as FanLeshowOperationMode, ) +from miio.airpurifier_airdog import ( # pylint: disable=import-error, import-error + OperationMode as AirDogOperationMode, +) from miio.fan_miot import ( OperationModeMiot as FanOperationModeMiot ) @@ -116,6 +122,9 @@ MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3" +MODEL_AIRPURIFIER_AIRDOG_X3 = "airdog.airpurifier.x3" +MODEL_AIRPURIFIER_AIRDOG_X5 = "airdog.airpurifier.x5" +MODEL_AIRPURIFIER_AIRDOG_X7SM = "airdog.airpurifier.x7sm" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" @@ -167,6 +176,9 @@ MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H, + MODEL_AIRPURIFIER_AIRDOG_X3, + MODEL_AIRPURIFIER_AIRDOG_X5, + MODEL_AIRPURIFIER_AIRDOG_X7SM, MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CA4, @@ -296,6 +308,11 @@ PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H] HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4] +# AirDogX7SM +ATTR_FORMALDEHYDE = "hcho" +# AirDogX3, AirDogX5, AirDogX7SM +ATTR_CLEAN_FILTERS = "clean_filters" + # Map attributes to properties of the state object AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { ATTR_TEMPERATURE: "temperature", @@ -587,6 +604,23 @@ ATTR_ERROR_DETECTED: "error_detected", } +AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3 = { + ATTR_MODE: "mode", + ATTR_SPEED: "speed", + ATTR_CHILD_LOCK: "child_lock", + ATTR_CLEAN_FILTERS: "clean_filters", + ATTR_PM25: "pm25", +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X5 = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3, +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X7SM = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3, + ATTR_FORMALDEHYDE: "hcho", +} + AVAILABLE_ATTRIBUTES_FAN_1C = { ATTR_MODE: "mode", ATTR_RAW_SPEED: "speed", @@ -830,6 +864,8 @@ FEATURE_FLAGS_FAN_LESHOW_SS4 = FEATURE_SET_BUZZER FEATURE_FLAGS_FAN_1C = FEATURE_FLAGS_FAN +FEATURE_FLAGS_AIRPURIFIER_AIRDOG = FEATURE_SET_CHILD_LOCK + SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" SERVICE_SET_FAN_LED_ON = "fan_set_led_on" @@ -850,6 +886,7 @@ SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" SERVICE_SET_DRY_ON = "fan_set_dry_on" SERVICE_SET_DRY_OFF = "fan_set_dry_off" +SERVICE_SET_FILTERS_CLEANED = "fan_set_filters_cleaned" # Airhumidifer CA4 SERVICE_SET_CLEAN_MODE_ON = "fan_set_clean_mode_on" @@ -1007,6 +1044,7 @@ SERVICE_SET_WET_PROTECTION_OFF: {"method": "async_set_wet_protection_off"}, SERVICE_SET_CLEAN_MODE_ON: {"method": "async_set_clean_mode_on"}, SERVICE_SET_CLEAN_MODE_OFF: {"method": "async_set_clean_mode_off"}, + SERVICE_SET_FILTERS_CLEANED: {"method": "async_set_filters_cleaned"}, } @@ -1095,6 +1133,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= elif model == MODEL_FAN_LESHOW_SS4: fan = FanLeshow(host, token, model=model) device = XiaomiFanLeshow(name, fan, model, unique_id, retries) + elif model == MODEL_AIRPURIFIER_AIRDOG_X3: + air_purifier = AirDogX3(host, token) + device = XiaomiAirDog(name, air_purifier, model, unique_id, retries) + elif model == MODEL_AIRPURIFIER_AIRDOG_X5: + air_purifier = AirDogX5(host, token) + device = XiaomiAirDog(name, air_purifier, model, unique_id, retries) + elif model == MODEL_AIRPURIFIER_AIRDOG_X7SM: + air_purifier = AirDogX7SM(host, token) + device = XiaomiAirDog(name, air_purifier, model, unique_id, retries) elif model == MODEL_FAN_1C: fan = FanC1(host, token, model=model) device = XiaomiFan1C(name, fan, model, unique_id, retries) @@ -2805,3 +2852,210 @@ async def async_set_natural_mode_off(self): self._device.set_mode, FanOperationModeMiot.Normal, ) + + +class XiaomiAirDog(XiaomiGenericDevice): + """Representation of a Xiaomi AirDog air purifiers.""" + + def __init__(self, name, device, model, unique_id, retries=0): + """Initialize the plug switch.""" + super().__init__(name, device, model, unique_id, retries) + + self._device_features = FEATURE_FLAGS_AIRPURIFIER_AIRDOG + + if self._model == MODEL_AIRPURIFIER_AIRDOG_X7SM: + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X7SM + else: + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3 + + self._preset_modes_to_mode_speed = { + 'Auto': (AirDogOperationMode('auto'), 1), + 'Night mode': (AirDogOperationMode('sleep'), 1), + 'Speed 1': (AirDogOperationMode('manual'), 1), + 'Speed 2': (AirDogOperationMode('manual'), 2), + 'Speed 3': (AirDogOperationMode('manual'), 3), + 'Speed 4': (AirDogOperationMode('manual'), 4), + } + if self._model == MODEL_AIRPURIFIER_AIRDOG_X7SM: + self._preset_modes_to_mode_speed['Speed 5'] = (AirDogOperationMode('Manual'), 5) + + self._mode_speed_to_preset_modes = {} + for key, value in self._preset_modes_to_mode_speed.items(): + self._mode_speed_to_preset_modes[value] = key + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes} + ) + + async def async_update(self): + """Fetch state from the device.""" + # On state change the device doesn't provide the new state immediately. + if self._skip_update: + self._skip_update = False + return + + try: + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._state_attrs.update( + { + key: self._extract_value_from_attribute(state, value) + for key, value in self._available_attributes.items() + } + ) + + self._retry = 0 + + except DeviceException as ex: + self._retry = self._retry + 1 + if self._retry < self._retries: + _LOGGER.info( + "Got exception while fetching the state: %s , _retry=%s", + ex, + self._retry, + ) + else: + self._available = False + _LOGGER.error( + "Got exception while fetching the state: %s , _retry=%s", + ex, + self._retry, + ) + + @property + def preset_modes(self): + """Get the list of available preset modes.""" + return list(self._preset_modes_to_mode_speed.keys()) + + @property + def preset_mode(self): + """Get the current preset mode.""" + if self._state: + # There are invalid modes, such as 'Auto 2'. There are no presets for them + if (AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED]) in self._mode_speed_to_preset_modes: + return self._mode_speed_to_preset_modes[(AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED])] + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + _LOGGER.debug("Setting the preset mode to: %s", preset_mode) + _LOGGER.debug("Calling set_mode_and_speed with parameters: %s", self._preset_modes_to_mode_speed[preset_mode]) + + # Following is true on AirDogX5 with firmware 1.3.5_0005. Maybe this is different for other models. Needs testing + + # It looks like the device was not designed to switch from any arbitrary mode to any other mode. + # Some of the combinations produce unexpected results + # + # For example, switching from 'Auto' to 'Speed X' switches to Manual mode, but always sets speed to 1, regardless of the speed parameter. + # + # Switching from 'Night mode' to 'Speed X' sets device in Auto mode with speed X. + # Tihs 'Auto X' state is quite strange and does not seem to be useful. + # Furthermore, we request Manual mode and get Auto. + # Switching from 'Auto X' mode to 'Manual X' works just fine. + # Switching from 'Auto X' mode to 'Manual Y' switches to 'Manual X'. + + # Here is a full table of device behaviour + + # FROM TO RESULT + #'Night mode' -> + # 'Auto' Good + # 'Speed 1' 'Auto 1' + repeat -> Good + # 'Speed 2' 'Auto 2' + repeat -> Good + # 'Speed 3' 'Auto 3' + repeat -> Good + # 'Speed 4' 'Auto 4' + repeat -> Good + #'Speed 1' + # 'Night mode' Good + # 'Auto' Good + #'Speed 2' -> + # 'Night mode' Good + # 'Auto' Good + #'Speed 3' -> + # 'Night mode' Good + # 'Auto' Good + #'Speed 4' -> + # 'Night mode' Good + #'Auto'-> + # 'Night mode' Good + # 'Speed 1' Good + # 'Speed 2' 'Speed 1' + repeat -> Good + # 'Speed 3' 'Speed 1' + repeat -> Good + # 'Speed 4' 'Speed 1' + repeat -> Good + + + # To allow switching from any mode to any other mode command is repeated twice when switching is from 'Night mode' or 'Auto' to 'Speed X'. + + await self._try_command( + "Setting preset mode of the miio device failed.", + self._device.set_mode_and_speed, + *self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple + ) + + if self._state_attrs[ATTR_MODE] in ('auto', 'sleep') and self._preset_modes_to_mode_speed[preset_mode][0].value == 'manual': + await self._try_command( + "Setting preset mode of the miio device failed.", + self._device.set_mode_and_speed, + *self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple + ) + + self._state_attrs.update( + { + ATTR_MODE: self._preset_modes_to_mode_speed[preset_mode][0].value, + ATTR_SPEED: self._preset_modes_to_mode_speed[preset_mode][1], + } + ) + self._skip_update = True + + async def async_set_filters_cleaned(self): + """Set filters cleaned.""" + await self._try_command( + "Setting filters cleaned failed.", + self._device.set_filters_cleaned, + ) + + + + async def async_turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turn the device on.""" + await super().async_turn_on(speed, percentage, preset_mode, **kwargs) + + self._state = True + self._skip_update = True + + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + await super().async_turn_off(**kwargs) + + self._state = False + self._skip_update = True + + + async def async_set_child_lock_on(self): + """Turn the child lock on.""" + await super().async_set_child_lock_on() + self._state_attrs.update( + { + ATTR_CHILD_LOCK: True, + } + ) + self._skip_update = True + + + async def async_set_child_lock_off(self): + """Turn the child lock off.""" + await super().async_set_child_lock_off() + self._state_attrs.update( + { + ATTR_CHILD_LOCK: False, + } + ) + self._skip_update = True diff --git a/custom_components/xiaomi_miio_airpurifier/services.yaml b/custom_components/xiaomi_miio_airpurifier/services.yaml index d77a25f..bd93de8 100644 --- a/custom_components/xiaomi_miio_airpurifier/services.yaml +++ b/custom_components/xiaomi_miio_airpurifier/services.yaml @@ -148,3 +148,12 @@ fan_set_dry_off: entity_id: description: Name of the xiaomi miio entity. example: "xiaomi_miio_airpurifier.xiaomi_miio_device" + +fan_set_filters_cleaned: + description: Inform the device that filters are cleaned. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: "xiaomi_miio_airpurifier.xiaomi_miio_device" + + From e141d73a06359bd57c4ca2d91a2326437696995b Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 6 May 2021 22:29:23 +0200 Subject: [PATCH 6/9] Add supported Air Dog devices --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 89c7ac2..d489194 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ This custom component is more or less the beta version of the [official componen | Air Purifier Super 2 | zhimi.airpurifier.sa2 | | | | Air Purifier 3 (2019) | zhimi.airpurifier.ma4 | | | | Air Purifier 3H (2019) | zhimi.airpurifier.mb3 | FJY4031GL(?), XM200017 | 45m2, 380m3/h CADR, 64dB, 38W (max) | +| Air Dog X3 | airdog.airpurifier.x3 | | | +| Air Dog X5 | airdog.airpurifier.x5 | | | +| Air Dog X7SM | airdog.airpurifier.x7sm | | | | Air Humidifier | zhimi.humidifier.v1 | | | | Air Humidifier CA1 | [zhimi.humidifier.ca1](docs/zhimi.humidifier.ca1.yaml) | | | | Smartmi Humidifier Evaporator 2 | zhimi.humidifier.ca4 | CJXJSQ04ZM | | From 0f21c97e0a905bc98e9898b49cc6c7f4588b39f7 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 6 May 2021 22:29:23 +0200 Subject: [PATCH 7/9] Add supported Air Dog devices --- README.md | 3 + .../xiaomi_miio_airpurifier/fan.py | 76 +++++++++++-------- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 89c7ac2..d489194 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ This custom component is more or less the beta version of the [official componen | Air Purifier Super 2 | zhimi.airpurifier.sa2 | | | | Air Purifier 3 (2019) | zhimi.airpurifier.ma4 | | | | Air Purifier 3H (2019) | zhimi.airpurifier.mb3 | FJY4031GL(?), XM200017 | 45m2, 380m3/h CADR, 64dB, 38W (max) | +| Air Dog X3 | airdog.airpurifier.x3 | | | +| Air Dog X5 | airdog.airpurifier.x5 | | | +| Air Dog X7SM | airdog.airpurifier.x7sm | | | | Air Humidifier | zhimi.humidifier.v1 | | | | Air Humidifier CA1 | [zhimi.humidifier.ca1](docs/zhimi.humidifier.ca1.yaml) | | | | Smartmi Humidifier Evaporator 2 | zhimi.humidifier.ca4 | CJXJSQ04ZM | | diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index 0283cc1..6dfddbe 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -5,6 +5,9 @@ import logging from miio import ( # pylint: disable=import-error + AirDogX3, + AirDogX5, + AirDogX7SM, AirFresh, AirFreshA1, AirFreshT2017, @@ -17,12 +20,9 @@ Device, DeviceException, Fan, + FanC1, FanLeshow, FanP5, - AirDogX3, - AirDogX5, - AirDogX7SM, - FanC1, FanP9, FanP10, FanP11, @@ -56,6 +56,9 @@ LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, ) +from miio.airpurifier_airdog import ( # pylint: disable=import-error, import-error + OperationMode as AirDogOperationMode, +) from miio.airpurifier_miot import ( # pylint: disable=import-error, import-error LedBrightness as AirpurifierMiotLedBrightness, OperationMode as AirpurifierMiotOperationMode, @@ -68,12 +71,7 @@ from miio.fan_leshow import ( # pylint: disable=import-error, import-error OperationMode as FanLeshowOperationMode, ) -from miio.airpurifier_airdog import ( # pylint: disable=import-error, import-error - OperationMode as AirDogOperationMode, -) -from miio.fan_miot import ( - OperationModeMiot as FanOperationModeMiot -) +from miio.fan_miot import OperationModeMiot as FanOperationModeMiot import voluptuous as vol from homeassistant.components.fan import ( @@ -864,7 +862,7 @@ FEATURE_FLAGS_FAN_LESHOW_SS4 = FEATURE_SET_BUZZER FEATURE_FLAGS_FAN_1C = FEATURE_FLAGS_FAN -FEATURE_FLAGS_AIRPURIFIER_AIRDOG = FEATURE_SET_CHILD_LOCK +FEATURE_FLAGS_AIRPURIFIER_AIRDOG = FEATURE_SET_CHILD_LOCK SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" @@ -2853,7 +2851,7 @@ async def async_set_natural_mode_off(self): FanOperationModeMiot.Normal, ) - + class XiaomiAirDog(XiaomiGenericDevice): """Representation of a Xiaomi AirDog air purifiers.""" @@ -2869,15 +2867,18 @@ def __init__(self, name, device, model, unique_id, retries=0): self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_AIRDOG_X3 self._preset_modes_to_mode_speed = { - 'Auto': (AirDogOperationMode('auto'), 1), - 'Night mode': (AirDogOperationMode('sleep'), 1), - 'Speed 1': (AirDogOperationMode('manual'), 1), - 'Speed 2': (AirDogOperationMode('manual'), 2), - 'Speed 3': (AirDogOperationMode('manual'), 3), - 'Speed 4': (AirDogOperationMode('manual'), 4), + "Auto": (AirDogOperationMode("auto"), 1), + "Night mode": (AirDogOperationMode("sleep"), 1), + "Speed 1": (AirDogOperationMode("manual"), 1), + "Speed 2": (AirDogOperationMode("manual"), 2), + "Speed 3": (AirDogOperationMode("manual"), 3), + "Speed 4": (AirDogOperationMode("manual"), 4), } if self._model == MODEL_AIRPURIFIER_AIRDOG_X7SM: - self._preset_modes_to_mode_speed['Speed 5'] = (AirDogOperationMode('Manual'), 5) + self._preset_modes_to_mode_speed["Speed 5"] = ( + AirDogOperationMode("Manual"), + 5, + ) self._mode_speed_to_preset_modes = {} for key, value in self._preset_modes_to_mode_speed.items(): @@ -2935,15 +2936,26 @@ def preset_mode(self): """Get the current preset mode.""" if self._state: # There are invalid modes, such as 'Auto 2'. There are no presets for them - if (AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED]) in self._mode_speed_to_preset_modes: - return self._mode_speed_to_preset_modes[(AirDogOperationMode(self._state_attrs[ATTR_MODE]), self._state_attrs[ATTR_SPEED])] + if ( + AirDogOperationMode(self._state_attrs[ATTR_MODE]), + self._state_attrs[ATTR_SPEED], + ) in self._mode_speed_to_preset_modes: + return self._mode_speed_to_preset_modes[ + ( + AirDogOperationMode(self._state_attrs[ATTR_MODE]), + self._state_attrs[ATTR_SPEED], + ) + ] return None async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" _LOGGER.debug("Setting the preset mode to: %s", preset_mode) - _LOGGER.debug("Calling set_mode_and_speed with parameters: %s", self._preset_modes_to_mode_speed[preset_mode]) + _LOGGER.debug( + "Calling set_mode_and_speed with parameters: %s", + self._preset_modes_to_mode_speed[preset_mode], + ) # Following is true on AirDogX5 with firmware 1.3.5_0005. Maybe this is different for other models. Needs testing @@ -2958,7 +2970,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: # Switching from 'Auto X' mode to 'Manual X' works just fine. # Switching from 'Auto X' mode to 'Manual Y' switches to 'Manual X'. - # Here is a full table of device behaviour + # Here is a full table of device behaviour # FROM TO RESULT #'Night mode' -> @@ -2985,20 +2997,26 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: # 'Speed 3' 'Speed 1' + repeat -> Good # 'Speed 4' 'Speed 1' + repeat -> Good - # To allow switching from any mode to any other mode command is repeated twice when switching is from 'Night mode' or 'Auto' to 'Speed X'. await self._try_command( "Setting preset mode of the miio device failed.", self._device.set_mode_and_speed, - *self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple + *self._preset_modes_to_mode_speed[ + preset_mode + ], # Corresponding mode and speed parameters are in tuple ) - if self._state_attrs[ATTR_MODE] in ('auto', 'sleep') and self._preset_modes_to_mode_speed[preset_mode][0].value == 'manual': + if ( + self._state_attrs[ATTR_MODE] in ("auto", "sleep") + and self._preset_modes_to_mode_speed[preset_mode][0].value == "manual" + ): await self._try_command( "Setting preset mode of the miio device failed.", self._device.set_mode_and_speed, - *self._preset_modes_to_mode_speed[preset_mode], # Corresponding mode and speed parameters are in tuple + *self._preset_modes_to_mode_speed[ + preset_mode + ], # Corresponding mode and speed parameters are in tuple ) self._state_attrs.update( @@ -3016,8 +3034,6 @@ async def async_set_filters_cleaned(self): self._device.set_filters_cleaned, ) - - async def async_turn_on( self, speed: str = None, @@ -3038,7 +3054,6 @@ async def async_turn_off(self, **kwargs) -> None: self._state = False self._skip_update = True - async def async_set_child_lock_on(self): """Turn the child lock on.""" await super().async_set_child_lock_on() @@ -3049,7 +3064,6 @@ async def async_set_child_lock_on(self): ) self._skip_update = True - async def async_set_child_lock_off(self): """Turn the child lock off.""" await super().async_set_child_lock_off() From 03614ec27d526bf854214f3660d10447cf08ab8e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 6 May 2021 22:49:23 +0200 Subject: [PATCH 8/9] Add dmaker.fan.p8 support (Closes: #26) --- README.md | 1 + custom_components/xiaomi_miio_airpurifier/fan.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d489194..31a6adb 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ This custom component is more or less the beta version of the [official componen | Pedestal Fan Fan ZA4 | zhimi.fan.za4 | | | | Pedestal Fan Fan 1C | dmaker.fan.1c | | | | Pedestal Fan Fan P5 | dmaker.fan.p5 | | | +| Pedestal Fan Fan P8 | dmaker.fan.p8 | | | | Pedestal Fan Fan P9 | dmaker.fan.p9 | | | | Pedestal Fan Fan P10 | dmaker.fan.p10 | | | | Mijia Pedestal Fan | dmaker.fan.p11 | BPLDS03DM | 2800mAh, 24W, <=58dB | diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index 0283cc1..b0797ba 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -147,6 +147,7 @@ MODEL_FAN_ZA3 = "zhimi.fan.za3" MODEL_FAN_ZA4 = "zhimi.fan.za4" MODEL_FAN_P5 = "dmaker.fan.p5" +MODEL_FAN_P8 = "dmaker.fan.p8" MODEL_FAN_P9 = "dmaker.fan.p9" MODEL_FAN_P10 = "dmaker.fan.p10" MODEL_FAN_P11 = "dmaker.fan.p11" @@ -198,6 +199,7 @@ MODEL_FAN_ZA3, MODEL_FAN_ZA4, MODEL_FAN_P5, + MODEL_FAN_P8, MODEL_FAN_P9, MODEL_FAN_P10, MODEL_FAN_P11, @@ -1142,7 +1144,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= elif model == MODEL_AIRPURIFIER_AIRDOG_X7SM: air_purifier = AirDogX7SM(host, token) device = XiaomiAirDog(name, air_purifier, model, unique_id, retries) - elif model == MODEL_FAN_1C: + elif model in [MODEL_FAN_1C, MODEL_FAN_P8]: fan = FanC1(host, token, model=model) device = XiaomiFan1C(name, fan, model, unique_id, retries) else: From e4f322942d7522ed7cff9968ed5e56318a86f80e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 7 May 2021 22:08:18 +0200 Subject: [PATCH 9/9] Fix typo --- custom_components/xiaomi_miio_airpurifier/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/xiaomi_miio_airpurifier/fan.py b/custom_components/xiaomi_miio_airpurifier/fan.py index 6a8cc12..2b46c56 100644 --- a/custom_components/xiaomi_miio_airpurifier/fan.py +++ b/custom_components/xiaomi_miio_airpurifier/fan.py @@ -20,7 +20,7 @@ Device, DeviceException, Fan, - FanC1, + Fan1C, FanLeshow, FanP5, FanP9, @@ -1143,7 +1143,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= air_purifier = AirDogX7SM(host, token) device = XiaomiAirDog(name, air_purifier, model, unique_id, retries) elif model in [MODEL_FAN_1C, MODEL_FAN_P8]: - fan = FanC1(host, token, model=model) + fan = Fan1C(host, token, model=model) device = XiaomiFan1C(name, fan, model, unique_id, retries) else: _LOGGER.error(