From 85e078f9476894b5922f20c6324b3964d98bdfc5 Mon Sep 17 00:00:00 2001 From: supp Date: Sun, 28 Nov 2021 17:44:00 +0300 Subject: [PATCH 1/9] Add support of the deerma.humidifier.jsqs Support of the deerma.humidifier.jsqs by example of the humidifier miot --- miio/__init__.py | 1 + miio/airhumidifier_jsqs.py | 250 +++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 miio/airhumidifier_jsqs.py diff --git a/miio/__init__.py b/miio/__init__.py index c3595b536..9700fbfe7 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -26,6 +26,7 @@ from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 from miio.airhumidifier_jsq import AirHumidifierJsq from miio.airhumidifier_miot import AirHumidifierMiot +from miio.airhumidifier_jsqs import AirHumidifierJsqs from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier from miio.airpurifier_airdog import AirDogX3, AirDogX5, AirDogX7SM diff --git a/miio/airhumidifier_jsqs.py b/miio/airhumidifier_jsqs.py new file mode 100644 index 000000000..4427ce8a7 --- /dev/null +++ b/miio/airhumidifier_jsqs.py @@ -0,0 +1,250 @@ +import enum +import logging +from typing import Any, Dict, Optional + +import click + +from .click_common import EnumType, command, format_output +from .exceptions import DeviceException +from .miot_device import DeviceStatus, MiotDevice + +_LOGGER = logging.getLogger(__name__) +_MAPPING = { + # Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:deerma-jsqs:2 + # Air Humidifier (siid=2) + "power": {"siid": 2, "piid": 1}, # bool + "fault": {"siid": 2, "piid": 2}, # 0 + "mode": {"siid": 2, "piid": 5}, # 1 - lvl1, 2 - lvl2, 3 - lvl3, 4 - auto + "target_humidity": {"siid": 2, "piid": 6}, # [40, 80] step 1 + # Environment (siid=3) + "relative_humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1 + "temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 1 + # Alarm (siid=5) + "buzzer": {"siid": 5, "piid": 1}, # bool + # Light (siid=6) + "led_light": {"siid": 6, "piid": 1}, # bool + # Other (siid=7) + "water_shortage_fault": {"siid": 7, "piid": 1}, # bool + "tank_filed": {"siid": 7, "piid": 2}, # bool + "overwet_protect": {"siid": 7, "piid": 3}, # bool +} + + +class AirHumidifierMiotException(DeviceException): + pass + + +class OperationMode(enum.Enum): + Low = 1 + Mid = 2 + High = 3 + Auto = 4 + + +class AirHumidifierMiotStatus(DeviceStatus): + """Container for status reports from the air humidifier. + + Xiaomi Mi Smart Humidifer S (deerma.humidifier.jsqs) respone (MIoT format) + + [ + {'did': 'power', 'siid': 2, 'piid': 1, 'code': 0, 'value': True}, + {'did': 'fault', 'siid': 2, 'piid': 2, 'code': 0, 'value': 0}, + {'did': 'mode', 'siid': 2, 'piid': 5, 'code': 0, 'value': 1}, + {'did': 'target_humidity', 'siid': 2, 'piid': 6, 'code': 0, 'value': 50}, + {'did': 'relative_humidity', 'siid': 3, 'piid': 1, 'code': 0, 'value': 40}, + {'did': 'temperature', 'siid': 3, 'piid': 7, 'code': 0, 'value': 22.7}, + {'did': 'buzzer', 'siid': 5, 'piid': 1, 'code': 0, 'value': False}, + {'did': 'led_light', 'siid': 6, 'piid': 1, 'code': 0, 'value': True}, + {'did': 'water_shortage_fault', 'siid': 7, 'piid': 1, 'code': 0, 'value': False}, + {'did': 'tank_filed', 'siid': 7, 'piid': 2, 'code': 0, 'value': False}, + {'did': 'overwet_protect', 'siid': 7, 'piid': 3, 'code': 0, 'value': False} + ] + """ + + def __init__(self, data: Dict[str, Any]) -> None: + self.data = data + + # Air Humidifier + + @property + def is_on(self) -> bool: + """Return True if device is on.""" + return self.data["power"] + + @property + def power(self) -> str: + """Return power state.""" + return "on" if self.is_on else "off" + + @property + def error(self) -> int: + """Return error state.""" + return self.data["fault"] + + @property + def mode(self) -> OperationMode: + """Return current operation mode.""" + + try: + mode = OperationMode(self.data["mode"]) + except ValueError as e: + _LOGGER.exception("Cannot parse mode: %s", e) + return OperationMode.Auto + + return mode + + @property + def target_humidity(self) -> int: + """Return target humidity.""" + return self.data["target_humidity"] + + # Environment + + @property + def relative_humidity(self) -> int: + """Return current humidity.""" + return self.data["relative_humidity"] + + @property + def temperature(self) -> Optional[float]: + """Return current temperature, if available.""" + if self.data["temperature"] is not None: + return round(self.data["temperature"], 1) + return None + + # Alarm + + @property + def buzzer(self) -> Optional[bool]: + """Return True if buzzer is on.""" + if self.data["buzzer"] is not None: + return self.data["buzzer"] + return None + + # Indicator Light + + @property + def led_light(self) -> bool: + """Return status of the LED.""" + + if self.data["led_light"] is not None: + return self.data["led_light"] + return None + + # Other + + @property + def tank_filed(self) -> bool: + """Return the tank filed.""" + + if self.data["tank_filed"] is not None: + return self.data["tank_filed"] + return None + + @property + def water_shortage_fault(self) -> Optional[bool]: + """Return water shortage fault.""" + + if self.data["water_shortage_fault"] is not None: + return self.data["water_shortage_fault"] + return None + + + @property + def overwet_protect(self) -> Optional[bool]: + """Return True if overwet mode is active.""" + + if self.data["overwet_protect"] is not None: + return self.data["overwet_protect"] + return None + + +class AirHumidifierJsqs(MiotDevice): + """Main class representing the air humidifier which uses MIoT protocol.""" + + mapping = _MAPPING + + @command( + default_output=format_output( + "", + "Power: {result.power}\n" + "Error: {result.error}\n" + "Target Humidity: {result.target_humidity} %\n" + "Relative Humidity: {result.relative_humidity} %\n" + "Temperature: {result.temperature} °C\n" + "Water tank detached: {result.tank_filed}\n" + "Mode: {result.mode}\n" + "LED light: {result.led_light}\n" + "Buzzer: {result.buzzer}\n" + "Overwet protection: {result.overwet_protect}\n" + ) + ) + def status(self) -> AirHumidifierMiotStatus: + """Retrieve properties.""" + + return AirHumidifierMiotStatus( + { + prop["did"]: prop["value"] if prop["code"] == 0 else None + for prop in self.get_properties_for_mapping() + } + ) + + @command(default_output=format_output("Powering on")) + def on(self): + """Power on.""" + return self.set_property("power", True) + + @command(default_output=format_output("Powering off")) + def off(self): + """Power off.""" + return self.set_property("power", False) + + @command( + click.argument("humidity", type=int), + default_output=format_output("Setting target humidity {humidity}%"), + ) + def set_target_humidity(self, humidity: int): + """Set target humidity.""" + if humidity < 40 or humidity > 80: + raise AirHumidifierMiotException( + "Invalid target humidity: %s. Must be between 40 and 80" % humidity + ) + return self.set_property("target_humidity", humidity) + + @command( + click.argument("mode", type=EnumType(OperationMode)), + default_output=format_output("Setting mode to '{mode.value}'"), + ) + def set_mode(self, mode: OperationMode): + """Set working mode.""" + return self.set_property("mode", mode.value) + + @command( + click.argument("light", type=bool), + default_output=format_output( + lambda light: "Turning on LED light" if light else "Turning off LED light" + ), + ) + def set_light(self, light: bool): + """Set led light.""" + return self.set_property("led_light", light) + + @command( + click.argument("buzzer", type=bool), + default_output=format_output( + lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer" + ), + ) + def set_buzzer(self, buzzer: bool): + """Set buzzer on/off.""" + return self.set_property("buzzer", buzzer) + + @command( + click.argument("overwet", type=bool), + default_output=format_output( + lambda overwet: "Turning on overwet" if overwet else "Turning off overwet" + ), + ) + def set_overwet(self, overwet: bool): + """Set overwet mode on/off.""" + return self.set_property("overwet_protect", overwet) From 89f59ebfdff859ef1fa32ff0b065ed692d2a6c40 Mon Sep 17 00:00:00 2001 From: supp Date: Sat, 4 Dec 2021 16:23:04 +0300 Subject: [PATCH 2/9] Added AirHumidifierJsqs test --- miio/airhumidifier_jsqs.py | 17 ++-- miio/discovery.py | 2 + miio/tests/test_airhumidifier_jsqs.py | 140 ++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 miio/tests/test_airhumidifier_jsqs.py diff --git a/miio/airhumidifier_jsqs.py b/miio/airhumidifier_jsqs.py index 4427ce8a7..7bb91315a 100644 --- a/miio/airhumidifier_jsqs.py +++ b/miio/airhumidifier_jsqs.py @@ -20,7 +20,7 @@ "relative_humidity": {"siid": 3, "piid": 1}, # [0, 100] step 1 "temperature": {"siid": 3, "piid": 7}, # [-30, 100] step 1 # Alarm (siid=5) - "buzzer": {"siid": 5, "piid": 1}, # bool + "buzzer": {"siid": 5, "piid": 1}, # bool # Light (siid=6) "led_light": {"siid": 6, "piid": 1}, # bool # Other (siid=7) @@ -30,7 +30,7 @@ } -class AirHumidifierMiotException(DeviceException): +class AirHumidifierJsqsException(DeviceException): pass @@ -41,7 +41,7 @@ class OperationMode(enum.Enum): Auto = 4 -class AirHumidifierMiotStatus(DeviceStatus): +class AirHumidifierJsqsStatus(DeviceStatus): """Container for status reports from the air humidifier. Xiaomi Mi Smart Humidifer S (deerma.humidifier.jsqs) respone (MIoT format) @@ -149,7 +149,6 @@ def water_shortage_fault(self) -> Optional[bool]: return self.data["water_shortage_fault"] return None - @property def overwet_protect(self) -> Optional[bool]: """Return True if overwet mode is active.""" @@ -176,13 +175,13 @@ class AirHumidifierJsqs(MiotDevice): "Mode: {result.mode}\n" "LED light: {result.led_light}\n" "Buzzer: {result.buzzer}\n" - "Overwet protection: {result.overwet_protect}\n" + "Overwet protection: {result.overwet_protect}\n", ) ) - def status(self) -> AirHumidifierMiotStatus: + def status(self) -> AirHumidifierJsqsStatus: """Retrieve properties.""" - return AirHumidifierMiotStatus( + return AirHumidifierJsqsStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None for prop in self.get_properties_for_mapping() @@ -206,7 +205,7 @@ def off(self): def set_target_humidity(self, humidity: int): """Set target humidity.""" if humidity < 40 or humidity > 80: - raise AirHumidifierMiotException( + raise AirHumidifierJsqsException( "Invalid target humidity: %s. Must be between 40 and 80" % humidity ) return self.set_property("target_humidity", humidity) @@ -245,6 +244,6 @@ def set_buzzer(self, buzzer: bool): lambda overwet: "Turning on overwet" if overwet else "Turning off overwet" ), ) - def set_overwet(self, overwet: bool): + def set_overwet_protect(self, overwet: bool): """Set overwet mode on/off.""" return self.set_property("overwet_protect", overwet) diff --git a/miio/discovery.py b/miio/discovery.py index 313c489a9..5a30fdd51 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -21,6 +21,7 @@ AirFreshT2017, AirHumidifier, AirHumidifierJsq, + AirHumidifierJsqs, AirHumidifierMjjsq, AirPurifier, AirPurifierMiot, @@ -137,6 +138,7 @@ AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ ), "deerma-humidifier-jsq1": partial(AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_JSQ1), + "deerma-humidifier-jsqs": AirHumidifierJsqs, "yunmi-waterpuri-v2": WaterPurifier, "yunmi.waterpuri.lx9": WaterPurifierYunmi, "yunmi.waterpuri.lx11": WaterPurifierYunmi, diff --git a/miio/tests/test_airhumidifier_jsqs.py b/miio/tests/test_airhumidifier_jsqs.py new file mode 100644 index 000000000..399335ffc --- /dev/null +++ b/miio/tests/test_airhumidifier_jsqs.py @@ -0,0 +1,140 @@ +import pytest + +from miio import AirHumidifierJsqs +from miio.airhumidifier_jsqs import ( + AirHumidifierJsqsException, + OperationMode, +) + +from .dummies import DummyMiotDevice + +_INITIAL_STATE = { + "power": True, + "fault": 0, + "mode": 4, + "target_humidity": 60, + "temperature": 21.6, + "relative_humidity": 62, + "buzzer": False, + "led_light": True, + "water_shortage_fault": False, + "tank_filed": False, + "overwet_protect": True, +} + + +class DummyAirHumidifierJsqs(DummyMiotDevice, AirHumidifierJsqs): + def __init__(self, *args, **kwargs): + self.state = _INITIAL_STATE + self.return_values = { + "get_prop": self._get_state, + "set_power": lambda x: self._set_state("power", x), + "set_target_humidity": lambda x: self._set_state("target_humidity", x), + "set_mode": lambda x: self._set_state("mode", x), + "set_led_light": lambda x: self._set_state("led_light", x), + "set_buzzer": lambda x: self._set_state("buzzer", x), + "set_overwet_protect": lambda x: self._set_state("overwet_protect", x), + } + super().__init__(*args, **kwargs) + + +@pytest.fixture() +def dev(request): + yield DummyAirHumidifierJsqs() + + +def test_on(dev): + dev.off() # ensure off + assert dev.status().is_on is False + + dev.on() + assert dev.status().is_on is True + + +def test_off(dev): + dev.on() # ensure on + assert dev.status().is_on is True + + dev.off() + assert dev.status().is_on is False + + +def test_status(dev): + status = dev.status() + assert status.is_on is _INITIAL_STATE["power"] + assert status.error == _INITIAL_STATE["fault"] + assert status.mode == OperationMode(_INITIAL_STATE["mode"]) + assert status.target_humidity == _INITIAL_STATE["target_humidity"] + assert status.temperature == _INITIAL_STATE["temperature"] + assert status.relative_humidity == _INITIAL_STATE["relative_humidity"] + assert status.buzzer == _INITIAL_STATE["buzzer"] + assert status.led_light == _INITIAL_STATE["led_light"] + assert status.water_shortage_fault == _INITIAL_STATE["water_shortage_fault"] + assert status.tank_filed == _INITIAL_STATE["tank_filed"] + assert status.overwet_protect == _INITIAL_STATE["overwet_protect"] + + +def test_set_target_humidity(dev): + def target_humidity(): + return dev.status().target_humidity + + dev.set_target_humidity(40) + assert target_humidity() == 40 + dev.set_target_humidity(80) + assert target_humidity() == 80 + + with pytest.raises(AirHumidifierJsqsException): + dev.set_target_humidity(39) + + with pytest.raises(AirHumidifierJsqsException): + dev.set_target_humidity(81) + + +def test_set_mode(dev): + def mode(): + return dev.status().mode + + dev.set_mode(OperationMode.Auto) + assert mode() == OperationMode.Auto + + dev.set_mode(OperationMode.Low) + assert mode() == OperationMode.Low + + dev.set_mode(OperationMode.Mid) + assert mode() == OperationMode.Mid + + dev.set_mode(OperationMode.High) + assert mode() == OperationMode.High + + +def test_set_led_light(dev): + def led_light(): + return dev.status().led_light + + dev.set_light(True) + assert led_light() is True + + dev.set_light(False) + assert led_light() is False + + +def test_set_buzzer(dev): + def buzzer(): + return dev.status().buzzer + + dev.set_buzzer(True) + assert buzzer() is True + + dev.set_buzzer(False) + assert buzzer() is False + + +def test_set_overwet_protect(dev): + def overwet_protect(): + return dev.status().overwet_protect + + dev.set_overwet_protect(True) + assert overwet_protect() is True + + dev.set_overwet_protect(False) + assert overwet_protect() is False From fe060e8e7d430d85fe7fe639a292ed286a69a958 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 4 Dec 2021 14:40:21 +0100 Subject: [PATCH 3/9] Fix lint issues --- miio/__init__.py | 2 +- miio/airhumidifier_jsqs.py | 4 ++-- miio/tests/test_airhumidifier_jsqs.py | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/miio/__init__.py b/miio/__init__.py index 9700fbfe7..8a63737b9 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -25,8 +25,8 @@ from miio.airfresh_t2017 import AirFreshA1, AirFreshT2017 from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 from miio.airhumidifier_jsq import AirHumidifierJsq -from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_jsqs import AirHumidifierJsqs +from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier from miio.airpurifier_airdog import AirDogX3, AirDogX5, AirDogX7SM diff --git a/miio/airhumidifier_jsqs.py b/miio/airhumidifier_jsqs.py index 7bb91315a..0f6071bbb 100644 --- a/miio/airhumidifier_jsqs.py +++ b/miio/airhumidifier_jsqs.py @@ -124,7 +124,7 @@ def buzzer(self) -> Optional[bool]: # Indicator Light @property - def led_light(self) -> bool: + def led_light(self) -> Optional[bool]: """Return status of the LED.""" if self.data["led_light"] is not None: @@ -134,7 +134,7 @@ def led_light(self) -> bool: # Other @property - def tank_filed(self) -> bool: + def tank_filed(self) -> Optional[bool]: """Return the tank filed.""" if self.data["tank_filed"] is not None: diff --git a/miio/tests/test_airhumidifier_jsqs.py b/miio/tests/test_airhumidifier_jsqs.py index 399335ffc..aa0985802 100644 --- a/miio/tests/test_airhumidifier_jsqs.py +++ b/miio/tests/test_airhumidifier_jsqs.py @@ -1,10 +1,7 @@ import pytest from miio import AirHumidifierJsqs -from miio.airhumidifier_jsqs import ( - AirHumidifierJsqsException, - OperationMode, -) +from miio.airhumidifier_jsqs import AirHumidifierJsqsException, OperationMode from .dummies import DummyMiotDevice From 38a5e7455a6b4377e53a0a3a3ce71115c1132a75 Mon Sep 17 00:00:00 2001 From: supp Date: Sun, 19 Dec 2021 19:02:21 +0300 Subject: [PATCH 4/9] Move miio/airhumidifier_jsqs.py to miio/integrations/humidifier/deerma/ --- miio/__init__.py | 2 +- miio/integrations/humidifier/__init__.py | 0 .../humidifier/deerma/__init__.py | 0 .../humidifier/deerma}/airhumidifier_jsqs.py | 38 ++++++------------- .../humidifier/deerma/tests/__init__.py | 0 .../deerma}/tests/test_airhumidifier_jsqs.py | 7 +++- 6 files changed, 17 insertions(+), 30 deletions(-) create mode 100644 miio/integrations/humidifier/__init__.py create mode 100644 miio/integrations/humidifier/deerma/__init__.py rename miio/{ => integrations/humidifier/deerma}/airhumidifier_jsqs.py (88%) create mode 100644 miio/integrations/humidifier/deerma/tests/__init__.py rename miio/{ => integrations/humidifier/deerma}/tests/test_airhumidifier_jsqs.py (96%) diff --git a/miio/__init__.py b/miio/__init__.py index 8a63737b9..52bff9717 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -25,7 +25,6 @@ from miio.airfresh_t2017 import AirFreshA1, AirFreshT2017 from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 from miio.airhumidifier_jsq import AirHumidifierJsq -from miio.airhumidifier_jsqs import AirHumidifierJsqs from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier @@ -46,6 +45,7 @@ from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11 from miio.integrations.fan.leshow import FanLeshow from miio.integrations.fan.zhimi import Fan, FanZA5 +from miio.integrations.humidifier.deerma.airhumidifier_jsqs import AirHumidifierJsqs from miio.integrations.light.philips import ( Ceil, PhilipsBulb, diff --git a/miio/integrations/humidifier/__init__.py b/miio/integrations/humidifier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miio/integrations/humidifier/deerma/__init__.py b/miio/integrations/humidifier/deerma/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miio/airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py similarity index 88% rename from miio/airhumidifier_jsqs.py rename to miio/integrations/humidifier/deerma/airhumidifier_jsqs.py index 0f6071bbb..8a35b87b6 100644 --- a/miio/airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py @@ -4,9 +4,9 @@ import click -from .click_common import EnumType, command, format_output -from .exceptions import DeviceException -from .miot_device import DeviceStatus, MiotDevice +from miio.click_common import EnumType, command, format_output +from miio.exceptions import DeviceException +from miio.miot_device import DeviceStatus, MiotDevice _LOGGER = logging.getLogger(__name__) _MAPPING = { @@ -96,66 +96,50 @@ def mode(self) -> OperationMode: @property def target_humidity(self) -> int: """Return target humidity.""" - return self.data["target_humidity"] + return self.data.get("target_humidity") # Environment @property def relative_humidity(self) -> int: """Return current humidity.""" - return self.data["relative_humidity"] + return self.data.get("relative_humidity") @property def temperature(self) -> Optional[float]: """Return current temperature, if available.""" - if self.data["temperature"] is not None: - return round(self.data["temperature"], 1) - return None + return self.data.get("temperature") # Alarm @property def buzzer(self) -> Optional[bool]: """Return True if buzzer is on.""" - if self.data["buzzer"] is not None: - return self.data["buzzer"] - return None + return self.data.get("buzzer") # Indicator Light @property def led_light(self) -> Optional[bool]: """Return status of the LED.""" - - if self.data["led_light"] is not None: - return self.data["led_light"] - return None + return self.data.get("led_light") # Other @property def tank_filed(self) -> Optional[bool]: """Return the tank filed.""" - - if self.data["tank_filed"] is not None: - return self.data["tank_filed"] - return None + return self.data.get("tank_filed") @property def water_shortage_fault(self) -> Optional[bool]: """Return water shortage fault.""" - - if self.data["water_shortage_fault"] is not None: - return self.data["water_shortage_fault"] - return None + return self.data.get("water_shortage_fault") @property def overwet_protect(self) -> Optional[bool]: """Return True if overwet mode is active.""" - - if self.data["overwet_protect"] is not None: - return self.data["overwet_protect"] - return None + return self.data.get("overwet_protect") class AirHumidifierJsqs(MiotDevice): diff --git a/miio/integrations/humidifier/deerma/tests/__init__.py b/miio/integrations/humidifier/deerma/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/miio/tests/test_airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py similarity index 96% rename from miio/tests/test_airhumidifier_jsqs.py rename to miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py index aa0985802..ee41d8a4c 100644 --- a/miio/tests/test_airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py @@ -1,9 +1,12 @@ import pytest from miio import AirHumidifierJsqs -from miio.airhumidifier_jsqs import AirHumidifierJsqsException, OperationMode +from ..airhumidifier_jsqs import ( + AirHumidifierJsqsException, + OperationMode, +) -from .dummies import DummyMiotDevice +from miio.tests.dummies import DummyMiotDevice _INITIAL_STATE = { "power": True, From 2cd15498f425fe0c9f002dd988b49ba2078b1805 Mon Sep 17 00:00:00 2001 From: supp Date: Sun, 19 Dec 2021 19:04:10 +0300 Subject: [PATCH 5/9] Add _supported_models variable with model description --- miio/integrations/humidifier/deerma/airhumidifier_jsqs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py index 8a35b87b6..a4b7a793c 100644 --- a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py @@ -145,6 +145,8 @@ def overwet_protect(self) -> Optional[bool]: class AirHumidifierJsqs(MiotDevice): """Main class representing the air humidifier which uses MIoT protocol.""" + _supported_models = ["deerma.humidifier.jsqs"] + mapping = _MAPPING @command( From 6876354650f3d57f23f74b11e0047ccc07e82340 Mon Sep 17 00:00:00 2001 From: supp Date: Tue, 18 Jan 2022 20:33:24 +0300 Subject: [PATCH 6/9] Fix lint issues --- miio/integrations/humidifier/deerma/airhumidifier_jsqs.py | 4 ++-- .../humidifier/deerma/tests/test_airhumidifier_jsqs.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py index a4b7a793c..aa8e90339 100644 --- a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py @@ -94,14 +94,14 @@ def mode(self) -> OperationMode: return mode @property - def target_humidity(self) -> int: + def target_humidity(self) -> Optional[int]: """Return target humidity.""" return self.data.get("target_humidity") # Environment @property - def relative_humidity(self) -> int: + def relative_humidity(self) -> Optional[int]: """Return current humidity.""" return self.data.get("relative_humidity") diff --git a/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py index ee41d8a4c..5f4ea9f2c 100644 --- a/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/tests/test_airhumidifier_jsqs.py @@ -1,13 +1,10 @@ import pytest from miio import AirHumidifierJsqs -from ..airhumidifier_jsqs import ( - AirHumidifierJsqsException, - OperationMode, -) - from miio.tests.dummies import DummyMiotDevice +from ..airhumidifier_jsqs import AirHumidifierJsqsException, OperationMode + _INITIAL_STATE = { "power": True, "fault": 0, From 87e9f06aa0b58123140328b1d3c6d8cc928339eb Mon Sep 17 00:00:00 2001 From: supp Date: Fri, 21 Jan 2022 16:52:28 +0300 Subject: [PATCH 7/9] Add export of AirHumidifierJsqs in the deerma package --- miio/__init__.py | 2 +- miio/integrations/humidifier/deerma/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/miio/__init__.py b/miio/__init__.py index 52bff9717..790fcf1af 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -45,7 +45,7 @@ from miio.integrations.fan.dmaker import Fan1C, FanMiot, FanP5, FanP9, FanP10, FanP11 from miio.integrations.fan.leshow import FanLeshow from miio.integrations.fan.zhimi import Fan, FanZA5 -from miio.integrations.humidifier.deerma.airhumidifier_jsqs import AirHumidifierJsqs +from miio.integrations.humidifier.deerma import AirHumidifierJsqs from miio.integrations.light.philips import ( Ceil, PhilipsBulb, diff --git a/miio/integrations/humidifier/deerma/__init__.py b/miio/integrations/humidifier/deerma/__init__.py index e69de29bb..d07fde6a4 100644 --- a/miio/integrations/humidifier/deerma/__init__.py +++ b/miio/integrations/humidifier/deerma/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from .airhumidifier_jsqs import AirHumidifierJsqs From 8a4f41ceccefa79305933d655fc6437c509359d7 Mon Sep 17 00:00:00 2001 From: supp Date: Fri, 21 Jan 2022 16:53:14 +0300 Subject: [PATCH 8/9] Support deerma.humidifier.jsq5 --- miio/integrations/humidifier/deerma/airhumidifier_jsqs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py index aa8e90339..652b1fd15 100644 --- a/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py +++ b/miio/integrations/humidifier/deerma/airhumidifier_jsqs.py @@ -44,7 +44,7 @@ class OperationMode(enum.Enum): class AirHumidifierJsqsStatus(DeviceStatus): """Container for status reports from the air humidifier. - Xiaomi Mi Smart Humidifer S (deerma.humidifier.jsqs) respone (MIoT format) + Xiaomi Mi Smart Humidifer S (deerma.humidifier.[jsqs, jsq5]) respone (MIoT format) [ {'did': 'power', 'siid': 2, 'piid': 1, 'code': 0, 'value': True}, @@ -145,7 +145,7 @@ def overwet_protect(self) -> Optional[bool]: class AirHumidifierJsqs(MiotDevice): """Main class representing the air humidifier which uses MIoT protocol.""" - _supported_models = ["deerma.humidifier.jsqs"] + _supported_models = ["deerma.humidifier.jsqs", "deerma.humidifier.jsq5"] mapping = _MAPPING From 7c22ff8935c7cee2dbcf29cfb93498a86ae6e1db Mon Sep 17 00:00:00 2001 From: supp Date: Fri, 21 Jan 2022 16:56:40 +0300 Subject: [PATCH 9/9] Update README, support of Xiaomi Mi Smart Humidifier (jsqs, jsq5) --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index a7b15868a..f53dd8855 100644 --- a/README.rst +++ b/README.rst @@ -151,6 +151,7 @@ Supported devices - Qingping Air Monitor Lite (cgllc.airm.cgdn1) - Xiaomi Walkingpad A1 (ksmb.walkingpad.v3) - Xiaomi Smart Pet Water Dispenser (mmgg.pet_waterer.s1, s4) +- Xiaomi Mi Smart Humidifer S (jsqs, jsq5) *Feel free to create a pull request to add support for new devices as