From 1698d1ebfdc7a17a29950d0ba908ee568d795c24 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sat, 5 Feb 2022 22:54:53 +0300 Subject: [PATCH 1/8] feature: Support for Zero Fog DWZF(G)-4500Z (shuii.humidifier.jsq002)humidifier #1171 PR preparations for https://github.com/rytilahti/python-miio/issues/1171 --- README.rst | 2 +- miio/__init__.py | 1 + miio/airhumidifier_jsq.py | 409 ++++++++++++++++++----- miio/discovery.py | 5 +- miio/tests/test_airhumidifier_jsq.py | 469 +++++++++++++++++++++++---- 5 files changed, 736 insertions(+), 150 deletions(-) diff --git a/README.rst b/README.rst index 7ef7e88e2..c4c8af6e8 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ Supported devices - Xiaomi Universal IR Remote Controller (Chuangmi IR) - Xiaomi Mi Smart Pedestal Fan V2, V3, SA1, ZA1, ZA3, ZA4, ZA5 1C, P5, P9, P10, P11 - Xiaomi Rosou SS4 Ventilator (leshow.fan.ss4) -- Xiaomi Mi Air Humidifier V1, CA1, CA4, CB1, MJJSQ, JSQ, JSQ1, JSQ001 +- Xiaomi Mi Air Humidifier V1, CA1, CA4, CB1, MJJSQ, JSQ, JSQ1, JSQ001, JSQ002 - Xiaomi Mi Water Purifier (Basic support: Turn on & off) - Xiaomi Mi Water Purifier D1, C1 (Triple Setting) - Xiaomi PM2.5 Air Quality Monitor V1, B1, S1 diff --git a/miio/__init__.py b/miio/__init__.py index 790fcf1af..8b37ca4e3 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -25,6 +25,7 @@ from miio.airfresh_t2017 import AirFreshA1, AirFreshT2017 from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 from miio.airhumidifier_jsq import AirHumidifierJsq +from miio.airhumidifier_jsq import AirHumidifierJsq002 from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index 398f4aedf..304cafc21 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -12,19 +12,73 @@ # Xiaomi Zero Fog Humidifier MODEL_HUMIDIFIER_JSQ001 = "shuii.humidifier.jsq001" +# Xiaomi Zero Fog DWZF(G)-4500Z +MODEL_HUMIDIFIER_JSQ002 = "shuii.humidifier.jsq002" + +# Array of several common commands +SHARED_COMMANDS = { + MODEL_HUMIDIFIER_JSQ001: dict( + power_on_off="set_start", + buzzer_on_off="set_buzzer" + ), + MODEL_HUMIDIFIER_JSQ002: dict( + power_on_off="on_off", + buzzer_on_off="buzzer_on" + ) +} # Array of properties in same order as in humidifier response AVAILABLE_PROPERTIES = { MODEL_HUMIDIFIER_JSQ001: [ - "temperature", # (degrees, int) - "humidity", # (percentage, int) - "mode", # ( 0: Intelligent, 1: Level1, ..., 5:Level4) - "buzzer", # (0: off, 1: on) - "child_lock", # (0: off, 1: on) - "led_brightness", # (0: off, 1: low, 2: high) - "power", # (0: off, 1: on) - "no_water", # (0: enough, 1: add water) - "lid_opened", # (0: ok, 1: lid is opened) + # Example of 'Air Humidifier (shuii.humidifier.jsq001)' response: + # [24, 37, 3, 1, 0, 2, 0, 0, 0] + "temperature", # 24 (degrees, int) + "humidity", # 37 (percentage, int) + "mode", # 3 ( 0: Intelligent, 1: Level1, ..., 5:Level4) + "buzzer", # 1 (0: off, 1: on) + "child_lock", # 0 (0: off, 1: on) + "led_brightness", # 2 (0: off, 1: low, 2: high) + "power", # 0 (0: off, 1: on) + "no_water", # 0 (water level state - 0: enough, 1: add water) + "lid_opened", # 0 (0: ok, 1: lid is opened) + ], + MODEL_HUMIDIFIER_JSQ002: [ + # Example of Xiaomi 'Zero Fog DWZF(G)-4500Z` (shuii.humidifier.jsq002)' + # Model: shuii.humidifier.jsq002 + # Hardware version: ESP8266 + # Firmware version: 1.4.0 + # + # > fast_set [bea, lock, temperature, humidity, led] + # 0/1, 0/1, 25, 50, 0/1] + # + # Properties: + # > dev.send("get_props","") + # [1, 2, 36, 2, 46, 4, 1, 1, 1, 50, 51, 0] + + # res[0]=1 Values (0: off/sleep, 1: on); + "power", # CMD: on_off [int] + # res[1]=2 Values (1, 2, 3); fan speed in UI: {Gear: level 1, level 2, level 3}; + "mode", # CMD: set_gear [int] + # res[2]=36 Value (is % [int] ); Environment humidity; + "humidity", + # res[3]=2 Values (1, 2, 3) // Light 1 - Off; 2 - low; 3 - high + "led_brightness", # CMD: set_led [int] + # res[4]=26 Values(is "ambient temp degrees int"+20, i.e. 46 corresponds to 26); + "temperature", + # res[5]=4 (0,1,2,3,4,5) + "water_level", # Get cmd: corrected_water [] + # res[6]=1, Water heater values (0: off, 1: on) + "heat", # CMD: warm_on [int] + # res[7]=1 BeaPower values (0: off, 1: on) + 'buzzer', # CMD: buzzer_on [int] + # res[8]=1, Values (0: off, 1: on) + 'child_lock', # CMD: set_lock [int] + # res[9]=50 Values (water heater target temp in degrees, [int]: 30..60) + 'target_temperature', # CMD: set_temp [int] + # res[10]=51 Values (% [int]) + 'target_humidity', # CMD: set_humidity [int] + # res[11]=0 Values: We failed to find when it changes, is not lid opened event + 'reserved', # XXX: cmd rst_clean [] ? ] } @@ -43,21 +97,19 @@ class LedBrightness(enum.Enum): High = 2 -class AirHumidifierStatus(DeviceStatus): - """Container for status reports from the air humidifier jsq.""" +class OperationModeJsq002(enum.Enum): + Level1 = 1 + Level2 = 2 + Level3 = 3 - def __init__(self, data: Dict[str, Any]) -> None: - """Status of an Air Humidifier (shuii.humidifier.jsq001): - [24, 30, 1, 1, 0, 2, 0, 0, 0] +class LedBrightnessJsq002(enum.Enum): + Off = 1 + Low = 2 + High = 3 - Parsed by AirHumidifierJsq device as: - {'temperature': 24, 'humidity': 29, 'mode': 1, 'buzzer': 1, - 'child_lock': 0, 'led_brightness': 2, 'power': 0, 'no_water': 0, - 'lid_opened': 0} - """ - self.data = data +class AirHumidifierStatusJsqCommon(DeviceStatus): @property def power(self) -> str: """Power state.""" @@ -68,10 +120,39 @@ def is_on(self) -> bool: """True if device is turned on.""" return self.power == "on" + @property + def humidity(self) -> int: + """Current humidity in percent.""" + return self.data["humidity"] + + @property + def temperature(self) -> int: + """Current temperature in degree celsius.""" + return self.data["temperature"] + + @property + def buzzer(self) -> bool: + """True if buzzer is turned on.""" + return self.data["buzzer"] == 1 + + @property + def child_lock(self) -> bool: + """Return True if child lock is on.""" + return self.data["child_lock"] == 1 + + +class AirHumidifierStatus(AirHumidifierStatusJsqCommon): + """Container for status reports from the air humidifier jsq.""" + + def __init__(self, data: Dict[str, Any]) -> None: + """ + Status of an Air Humidifier (shuii.humidifier.jsq001): + """ + self.data = data + @property def mode(self) -> OperationMode: """Operation mode. - Can be either low, medium, high or humidity. """ @@ -83,21 +164,6 @@ def mode(self) -> OperationMode: return mode - @property - def temperature(self) -> int: - """Current temperature in degree celsius.""" - return self.data["temperature"] - - @property - def humidity(self) -> int: - """Current humidity in percent.""" - return self.data["humidity"] - - @property - def buzzer(self) -> bool: - """True if buzzer is turned on.""" - return self.data["buzzer"] == 1 - @property def led_brightness(self) -> LedBrightness: """Buttons illumination Brightness level.""" @@ -114,11 +180,6 @@ def led(self) -> bool: """True if LED is turned on.""" return self.led_brightness is not LedBrightness.Off - @property - def child_lock(self) -> bool: - """Return True if child lock is on.""" - return self.data["child_lock"] == 1 - @property def no_water(self) -> bool: """True if the water tank is empty.""" @@ -138,46 +199,105 @@ def use_time(self) -> Optional[int]: return None -class AirHumidifierJsq(Device): - """Implementation of Xiaomi Zero Fog Humidifier: shuii.humidifier.jsq001.""" +class AirHumidifierStatusJsq002(AirHumidifierStatusJsqCommon): + def __init__(self, data: Dict[str, Any]) -> None: + """ + Status of Xiaomi 'Zero Fog DWZF(G)-4500Z` (shuii.humidifier.jsq002): + """ + self.data = data + + @property + def mode(self) -> OperationModeJsq002: + """Operation mode. + + Can be either low, medium, high or humidity. + """ + + try: + mode = OperationModeJsq002(self.data["mode"]) + except ValueError as e: + _LOGGER.exception("Cannot parse mode: %s", e) + return OperationModeJsq002.Level1 + + return mode + + @property + def temperature(self) -> int: + """Current temperature in degree celsius.""" + + # Real temp is 'value - 20', were value from device + return self.data["temperature"] - 20 _supported_models = [MODEL_HUMIDIFIER_JSQ001] - @command( - default_output=format_output( - "", - "Power: {result.power}\n" - "Mode: {result.mode}\n" - "Temperature: {result.temperature} °C\n" - "Humidity: {result.humidity} %\n" - "Buzzer: {result.buzzer}\n" - "LED brightness: {result.led_brightness}\n" - "Child lock: {result.child_lock}\n" - "No water: {result.no_water}\n" - "Lid opened: {result.lid_opened}\n", - ) - ) - def status(self) -> AirHumidifierStatus: + @property + def water_level(self) -> int: + """Water level: 0,1,2,3,4,5""" + + level = self.data["water_level"] + if level not in [0, 1, 2, 3, 4, 5]: + _LOGGER.exception("Water level should be 0,1,2,3,4,5. But was: %s", str(level)) + return 5 + + return level + + @property + def heater(self) -> bool: + """Return True if child lock is on.""" + return self.data["heat"] == 1 + + @property + def water_target_temperature(self) -> int: + """Return Target Water Temperature, degrees C, 30..60.""" + target_temp = self.data["target_temperature"] + target_temp_int = int(target_temp) + if target_temp_int not in range(30, 61): + _LOGGER.exception("Target water heater temp should be in [30..60]. But was: %s", str(target_temp)) + return 30 + + return target_temp_int + + @property + def target_humidity(self) -> int: + """Return Target Water Temperature, degrees C, 30..60.""" + target_humidity = self.data["target_humidity"] + target_humidity_int = int(target_humidity) + if target_humidity_int not in range(0, 100): + _LOGGER.exception("Target humidity should be in [0..99]. But was: %s", str(target_humidity)) + return 30 + + return target_humidity_int + + @property + def led_brightness(self) -> LedBrightnessJsq002: + """Buttons illumination Brightness level.""" + try: + brightness = LedBrightnessJsq002(self.data["led_brightness"]) + except ValueError as e: + _LOGGER.exception("Cannot parse brightness: %s", e) + return LedBrightnessJsq002.Off + + return brightness + + @property + def no_water(self) -> bool: + """True if the water tank is empty.""" + return self.water_level == 0 + + +class AirHumidifierJsqCommon(Device): + _supported_models = sorted(AVAILABLE_PROPERTIES.keys()) + + def _get_props(self) -> Dict: """Retrieve properties.""" values = self.send("get_props") - # Response of an Air Humidifier (shuii.humidifier.jsq001): - # [24, 37, 3, 1, 0, 2, 0, 0, 0] - # - # status[0] : temperature (degrees, int) - # status[1]: humidity (percentage, int) - # status[2]: mode ( 0: Intelligent, 1: Level1, ..., 5:Level4) - # status[3]: buzzer (0: off, 1: on) - # status[4]: lock (0: off, 1: on) - # status[5]: brightness (0: off, 1: low, 2: high) - # status[6]: power (0: off, 1: on) - # status[7]: water level state (0: ok, 1: add water) - # status[8]: lid state (0: ok, 1: lid is opened) - - properties = AVAILABLE_PROPERTIES.get( - self.model, AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_JSQ001] - ) + if self.model not in AVAILABLE_PROPERTIES: + raise AirHumidifierException("Unsupported model: %s" % self.model) + + properties = AVAILABLE_PROPERTIES[self.model] + if len(properties) != len(values): _LOGGER.error( "Count (%s) of requested properties (%s) does not match the " @@ -188,17 +308,70 @@ def status(self) -> AirHumidifierStatus: values, ) - return AirHumidifierStatus({k: v for k, v in zip(properties, values)}) + return {k: v for k, v in zip(properties, values)} @command(default_output=format_output("Powering on")) def on(self): """Power on.""" - return self.send("set_start", [1]) + if self.model not in SHARED_COMMANDS: + raise AirHumidifierException("Unsupported model: %s" % self.model) + + return self.send(SHARED_COMMANDS[self.model]['power_on_off'], [1]) @command(default_output=format_output("Powering off")) def off(self): """Power off.""" - return self.send("set_start", [0]) + if self.model not in SHARED_COMMANDS: + raise AirHumidifierException("Unsupported model: %s" % self.model) + + return self.send(SHARED_COMMANDS[self.model]['power_on_off'], [0]) + + @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.""" + if self.model not in SHARED_COMMANDS: + raise AirHumidifierException("Unsupported model: %s" % self.model) + + return self.send( + SHARED_COMMANDS[self.model]['buzzer_on_off'], + [int(bool(buzzer))] + ) + + @command( + click.argument("lock", type=bool), + default_output=format_output( + lambda lock: "Turning on child lock" if lock else "Turning off child lock" + ), + ) + def set_child_lock(self, lock: bool): + """Set child lock on/off.""" + return self.send("set_lock", [int(bool(lock))]) + + +class AirHumidifierJsq(AirHumidifierJsqCommon): + """Implementation of Xiaomi Zero Fog Humidifier: shuii.humidifier.jsq001.""" + + @command( + default_output=format_output( + "", + "Power: {result.power}\n" + "Mode: {result.mode}\n" + "Temperature: {result.temperature} °C\n" + "Humidity: {result.humidity} %\n" + "Buzzer: {result.buzzer}\n" + "LED brightness: {result.led_brightness}\n" + "Child lock: {result.child_lock}\n" + "No water: {result.no_water}\n" + "Lid opened: {result.lid_opened}\n", + ) + ) + def status(self) -> AirHumidifierStatus: + return AirHumidifierStatus(self._get_props()) @command( click.argument("mode", type=EnumType(OperationMode)), @@ -235,15 +408,65 @@ def set_led(self, led: bool): brightness = LedBrightness.High if led else LedBrightness.Off return self.set_led_brightness(brightness) + +class AirHumidifierJsq002(AirHumidifierJsqCommon): @command( - click.argument("buzzer", type=bool), default_output=format_output( - lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer" + "", + "Power: {result.power}\n" + "Mode: {result.mode}\n" + "Temperature: {result.temperature} °C\n" + "Humidity: {result.humidity} %\n" + "Buzzer: {result.buzzer}\n" + "LED brightness: {result.led_brightness}\n" + "Child lock: {result.child_lock}\n" + "Water level: {result.water_level}\n" + "Water heater: {result.heater}\n" + "Water target temperature: {result.water_target_temperature} °C\n" + "Target humidity: {result.target_humidity} %\n" + ) + ) + def status(self) -> AirHumidifierStatusJsq002: + return AirHumidifierStatusJsq002(self._get_props()) + + @command( + click.argument("mode", type=EnumType(OperationModeJsq002)), + default_output=format_output("Setting mode to '{mode.value}'"), + ) + def set_mode(self, mode: OperationModeJsq002): + """Set mode.""" + value = mode.value + if value not in (om.value for om in OperationModeJsq002): + raise AirHumidifierException( + "{} is not a valid OperationModeJsq2 value".format(value) + ) + + return self.send("set_gear", [value]) + + @command( + click.argument("brightness", type=EnumType(LedBrightnessJsq002)), + default_output=format_output("Setting LED brightness to {brightness}"), + ) + def set_led_brightness(self, brightness: LedBrightnessJsq002): + """Set led brightness.""" + value = brightness.value + if value not in (lb.value for lb in LedBrightnessJsq002): + raise AirHumidifierException( + "{} is not a valid LedBrightnessJsq2 value".format(value) + ) + + return self.send("set_led", [value]) + + @command( + click.argument("led", type=bool), + default_output=format_output( + lambda led: "Turning on LED" if led else "Turning off LED" ), ) - def set_buzzer(self, buzzer: bool): - """Set buzzer on/off.""" - return self.send("set_buzzer", [int(bool(buzzer))]) + def set_led(self, led: bool): + """Turn led on/off.""" + brightness = LedBrightnessJsq002.High if led else LedBrightnessJsq002.Off + return self.set_led_brightness(brightness) @command( click.argument("lock", type=bool), @@ -251,6 +474,22 @@ def set_buzzer(self, buzzer: bool): lambda lock: "Turning on child lock" if lock else "Turning off child lock" ), ) - def set_child_lock(self, lock: bool): - """Set child lock on/off.""" - return self.send("set_lock", [int(bool(lock))]) + def set_heater(self, heater_on: bool): + """Set water heater on/off.""" + return self.send("warm_on", [int(bool(heater_on))]) + + def set_target_temperature(self, temperature: int): + """Set the target temperature degrees C, only 30..60.""" + + if temperature not in range(30, 61): + raise AirHumidifierException("Invalid water target temperature, should be in [30..60]. But was: %s" % temperature) + + return self.send("set_temp", [temperature]) + + def set_target_humidity(self, humidity: int): + """Set the target humidity %, only 0..99.""" + + if humidity not in range(0, 100): + raise AirHumidifierException("Invalid target humidity, should be in [0..99]. But was: %s" % humidity) + + return self.send("set_humidity", [humidity]) diff --git a/miio/discovery.py b/miio/discovery.py index 5a30fdd51..274eeb6dd 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -21,6 +21,7 @@ AirFreshT2017, AirHumidifier, AirHumidifierJsq, + AirHumidifierJsq002, AirHumidifierJsqs, AirHumidifierMjjsq, AirPurifier, @@ -62,6 +63,7 @@ MODEL_HUMIDIFIER_CB1, MODEL_HUMIDIFIER_V1, ) +from .airhumidifier_jsq import MODEL_HUMIDIFIER_JSQ001, MODEL_HUMIDIFIER_JSQ002 from .airhumidifier_mjjsq import MODEL_HUMIDIFIER_JSQ1, MODEL_HUMIDIFIER_MJJSQ from .airqualitymonitor import ( MODEL_AIRQUALITYMONITOR_B1, @@ -133,7 +135,8 @@ "zhimi-humidifier-v1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_V1), "zhimi-humidifier-ca1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CA1), "zhimi-humidifier-cb1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CB1), - "shuii-humidifier-jsq001": partial(AirHumidifierJsq, model=MODEL_HUMIDIFIER_MJJSQ), + "shuii-humidifier-jsq001": partial(AirHumidifierJsq, model=MODEL_HUMIDIFIER_JSQ001), + "shuii-humidifier-jsq002": partial(AirHumidifierJsq002, model=MODEL_HUMIDIFIER_JSQ002), "deerma-humidifier-mjjsq": partial( AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ ), diff --git a/miio/tests/test_airhumidifier_jsq.py b/miio/tests/test_airhumidifier_jsq.py index 60f3e2536..5d60e520d 100644 --- a/miio/tests/test_airhumidifier_jsq.py +++ b/miio/tests/test_airhumidifier_jsq.py @@ -3,19 +3,24 @@ import pytest -from miio import AirHumidifierJsq +from miio import AirHumidifierJsq,AirHumidifierJsq002 from miio.airhumidifier import AirHumidifierException from miio.airhumidifier_jsq import ( + AVAILABLE_PROPERTIES, MODEL_HUMIDIFIER_JSQ001, AirHumidifierStatus, LedBrightness, OperationMode, + MODEL_HUMIDIFIER_JSQ002, + AirHumidifierStatusJsq002, + OperationModeJsq002, + LedBrightnessJsq002 ) from .dummies import DummyDevice -class DummyAirHumidifierJsq(DummyDevice, AirHumidifierJsq): +class DummyAirHumidifierJsq001(DummyDevice, AirHumidifierJsq): def __init__(self, *args, **kwargs): self._model = MODEL_HUMIDIFIER_JSQ001 @@ -76,10 +81,64 @@ def _get_state(self, props): return list(self.state.values()) +class DummyAirHumidifierJsq002(DummyDevice, AirHumidifierJsq002): + def _set_state_by_key(self, key, value): + self._set_state( + self._available_properties.index(key), + value + ) + + def _get_state_by_key(self, key): + return self.state[self._available_properties.index(key)] + + def __init__(self, *args, **kwargs): + self._model = MODEL_HUMIDIFIER_JSQ002 + + self.dummy_device_info = { + "fw_ver": "1.4.0", + "hw_ver": "ESP8266", + } + + self.start_state = [1, 2, 36, 2, 46, 4, 1, 1, 1, 50, 51, 0] + self.state = self.start_state.copy() + + # Mocks for `device.send("cmd_name", args)` commands: + self._available_properties = AVAILABLE_PROPERTIES[self._model] + + self.return_values = { + "get_props": self._get_state, + "on_off": lambda x: self._set_state_by_key("power", x), + "set_gear": lambda x: self._set_state_by_key("mode", x), + "set_led": lambda x: self._set_state_by_key("led_brightness", x), + "warm_on": lambda x: self._set_state_by_key("heat", x), + "buzzer_on": lambda x: self._set_state_by_key("buzzer", x), + "set_lock": lambda x: self._set_state_by_key("child_lock", x), + "set_temp": lambda x: self._set_state_by_key("target_temperature", x), + "set_humidity": lambda x: self._set_state_by_key("target_humidity", x), + # "corrected_water": ? + # "rst_clean": ? + "miIO.info": self._get_device_info, + } + + super().__init__(args, kwargs) + + def _get_state(self, props): + """Mocks device `get_props` command """ + return self.state + + def _get_device_info(self, _): + """Return dummy device info.""" + return self.dummy_device_info + + +@pytest.fixture(scope="class") +def airhumidifier_jsq001(request): + request.cls.device = DummyAirHumidifierJsq001() + + @pytest.fixture(scope="class") -def airhumidifier_jsq(request): - request.cls.device = DummyAirHumidifierJsq() - # TODO add ability to test on a real device +def airhumidifier_jsq002(request): + request.cls.device = DummyAirHumidifierJsq002() class Bunch: @@ -87,27 +146,60 @@ def __init__(self, **kwds): self.__dict__.update(kwds) -@pytest.mark.usefixtures("airhumidifier_jsq") -class TestAirHumidifierJsq(TestCase): - def is_on(self): - return self.device.status().is_on - +class AirHumidifierJsqTestCase(TestCase): def state(self): return self.device.status() + def _is_on(self): + return self.device.status().is_on + + def do_test_on_off_property(self, setter_name, getter_name): + def prop_value(): + return getattr(self.device.status(), getter_name) + + prop_fun = getattr(self.device, setter_name) + prop_fun(True) + assert prop_value() is True + + prop_fun(False) + assert prop_value() is False + + # if user uses wrong type for buzzer value + prop_fun(1) + assert prop_value() is True + + prop_fun(0) + assert prop_value() is False + + prop_fun("not_empty_str") + assert prop_value() is True + + prop_fun("on") + assert prop_value() is True + + # all string values are considered to by True, even "off" + prop_fun("off") + assert prop_value() is True + + prop_fun("") + assert prop_value() is False + + +@pytest.mark.usefixtures("airhumidifier_jsq001") +class TestAirHumidifierJsq001(AirHumidifierJsqTestCase): def test_on(self): self.device.off() # ensure off - assert self.is_on() is False + assert self._is_on() is False self.device.on() - assert self.is_on() is True + assert self._is_on() is True def test_off(self): self.device.on() # ensure on - assert self.is_on() is True + assert self._is_on() is True self.device.off() - assert self.is_on() is False + assert self._is_on() is False def test_status(self): self.device._reset_state() @@ -122,7 +214,7 @@ def test_status(self): assert self.state().led_brightness == LedBrightness( self.device.start_state["led_brightness"] ) - assert self.is_on() is True + assert self._is_on() is True assert self.state().no_water == (self.device.start_state["no_water"] == 1) assert self.state().lid_opened == (self.device.start_state["lid_opened"] == 1) @@ -228,34 +320,7 @@ def led_brightness(): assert led_brightness() == LedBrightness.Off def test_set_buzzer(self): - def buzzer(): - return self.device.status().buzzer - - self.device.set_buzzer(True) - assert buzzer() is True - - self.device.set_buzzer(False) - assert buzzer() is False - - # if user uses wrong type for buzzer value - self.device.set_buzzer(1) - assert buzzer() is True - - self.device.set_buzzer(0) - assert buzzer() is False - - self.device.set_buzzer("not_empty_str") - assert buzzer() is True - - self.device.set_buzzer("on") - assert buzzer() is True - - # all string values are considered to by True, even "off" - self.device.set_buzzer("off") - assert buzzer() is True - - self.device.set_buzzer("") - assert buzzer() is False + self.do_test_on_off_property("set_buzzer", "buzzer") def test_status_without_temperature(self): self.device._reset_state() @@ -276,31 +341,309 @@ def test_status_without_mode(self): assert self.state().mode is OperationMode.Intelligent def test_set_child_lock(self): - def child_lock(): - return self.device.status().child_lock + self.do_test_on_off_property("set_child_lock", "child_lock") - self.device.set_child_lock(True) - assert child_lock() is True - self.device.set_child_lock(False) - assert child_lock() is False +@pytest.mark.usefixtures("airhumidifier_jsq002") +class TestAirHumidifierJsq002(AirHumidifierJsqTestCase): + def test_status(self): + self.device._reset_state() - # if user uses wrong type for buzzer value - self.device.set_child_lock(1) - assert child_lock() is True + state: AirHumidifierStatusJsq002 = self.state() + properties = AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_JSQ002] + assert repr(state) == repr(AirHumidifierStatusJsq002({ + k: v for k, v in zip(properties, self.device.start_state) + })) - self.device.set_child_lock(0) - assert child_lock() is False + assert state.data['power'] == 1 + assert self._is_on() is True - self.device.set_child_lock("not_empty_str") - assert child_lock() is True + assert state.data['mode'] == 2 + assert state.mode == OperationModeJsq002.Level2 - self.device.set_child_lock("on") - assert child_lock() is True + assert state.data['humidity'] == 36 + assert state.humidity == 36 - # all string values are considered to by True, even "off" - self.device.set_child_lock("off") - assert child_lock() is True + assert state.data['led_brightness'] == 2 + assert state.led_brightness == LedBrightnessJsq002.Low + + assert state.data['temperature'] == 46 + assert state.temperature == 26 + + assert state.data['water_level'] == 4 + assert state.water_level == 4 + + assert state.data['heat'] == 1 + assert state.heater is True + + assert state.data['buzzer'] == 1 + assert state.buzzer is True + + assert state.data['child_lock'] == 1 + assert state.child_lock is True + + assert state.data['target_temperature'] == 50 + assert state.water_target_temperature == 50 + + assert state.data['target_humidity'] == 51 + assert state.target_humidity == 51 + + assert state.data['reserved'] == 0 + + def test_on(self): + self.device.off() # ensure off + assert self._is_on() is False + + self.device.on() + assert self._is_on() is True + + def test_off(self): + self.device.on() # ensure on + assert self._is_on() is True + + self.device.off() + assert self._is_on() is False + + def test_status_wrong_input(self): + def mode(): + return self.device.status().mode + + def set_mock_state_mode(new_mode): + self.device._set_state_by_key("mode", [new_mode]) + + def set_mock_state_brightness(new_val): + self.device._set_state_by_key("led_brightness", [new_val]) + + def led_brightness(): + return self.device.status().led_brightness + + def water_level(): + return self.device.status().water_level + + def set_mock_water_level(new_val): + self.device._set_state_by_key("water_level", [new_val]) + + self.device._reset_state() + + set_mock_state_mode(10) + assert mode() == OperationModeJsq002.Level1 + + set_mock_state_mode(10) + assert mode() == OperationModeJsq002.Level1 - self.device.set_child_lock("") - assert child_lock() is False + set_mock_state_brightness(10) + assert led_brightness() == LedBrightnessJsq002.Off + + set_mock_state_brightness("smth") + assert led_brightness() == LedBrightnessJsq002.Off + + set_mock_water_level(10) + assert water_level() == 5 + + set_mock_water_level("smth") + assert water_level() == 5 + + def test_set_mode(self): + def mode(): + return self.device.status().mode + + self.device.set_mode(OperationModeJsq002.Level1) + assert mode() == OperationModeJsq002.Level1 + + self.device.set_mode(OperationModeJsq002.Level2) + assert mode() == OperationModeJsq002.Level2 + + self.device.set_mode(OperationModeJsq002.Level3) + assert mode() == OperationModeJsq002.Level3 + + def test_set_mode_wrong_input(self): + def mode(): + return self.device.status().mode + + self.device.set_mode(OperationModeJsq002.Level3) + assert mode() == OperationModeJsq002.Level3 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_mode(Bunch(value=10)) + assert str(excinfo.value) == "10 is not a valid OperationModeJsq2 value" + assert mode() == OperationModeJsq002.Level3 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_mode(Bunch(value=-1)) + assert str(excinfo.value) == "-1 is not a valid OperationModeJsq2 value" + assert mode() == OperationModeJsq002.Level3 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_mode(Bunch(value="smth")) + assert str(excinfo.value) == "smth is not a valid OperationModeJsq2 value" + assert mode() == OperationModeJsq002.Level3 + + def test_set_led_brightness(self): + def led_brightness(): + return self.device.status().led_brightness + + self.device.set_led_brightness(LedBrightnessJsq002.Off) + assert led_brightness() == LedBrightnessJsq002.Off + + self.device.set_led_brightness(LedBrightnessJsq002.Low) + assert led_brightness() == LedBrightnessJsq002.Low + + self.device.set_led_brightness(LedBrightnessJsq002.High) + assert led_brightness() == LedBrightnessJsq002.High + + def test_set_led_brightness_wrong_input(self): + def led_brightness(): + return self.device.status().led_brightness + + self.device.set_led_brightness(LedBrightnessJsq002.Low) + assert led_brightness() == LedBrightnessJsq002.Low + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_led_brightness(Bunch(value=10)) + assert str(excinfo.value) == "10 is not a valid LedBrightnessJsq2 value" + assert led_brightness() == LedBrightnessJsq002.Low + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_led_brightness(Bunch(value=-10)) + assert str(excinfo.value) == "-10 is not a valid LedBrightnessJsq2 value" + assert led_brightness() == LedBrightnessJsq002.Low + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_led_brightness(Bunch(value="smth")) + assert str(excinfo.value) == "smth is not a valid LedBrightnessJsq2 value" + assert led_brightness() == LedBrightnessJsq002.Low + + def test_set_led(self): + def led_brightness(): + return self.device.status().led_brightness + + self.device.set_led(True) + assert led_brightness() == LedBrightnessJsq002.High + + self.device.set_led(False) + assert led_brightness() == LedBrightnessJsq002.Off + + def test_no_water(self): + def set_mock_water_level(new_val): + self.device._set_state_by_key("water_level", [new_val]) + + def no_water(): + return self.device.status().no_water + + set_mock_water_level(1) + assert no_water() is False + + set_mock_water_level(0) + assert no_water() is True + + set_mock_water_level(6) + assert no_water() is False + + def test_set_heater(self): + self.do_test_on_off_property("set_heater", "heater") + + def test_set_buzzer(self): + self.do_test_on_off_property("set_buzzer", "buzzer") + + def test_set_child_lock(self): + self.do_test_on_off_property("set_child_lock", "child_lock") + + def test_set_target_temperature(self): + def water_target_temp(): + return self.device.status().water_target_temperature + + self.device.set_target_temperature(30) + assert water_target_temp() == 30 + + self.device.set_target_temperature(31) + assert water_target_temp() == 31 + + self.device.set_target_temperature(59) + assert water_target_temp() == 59 + + self.device.set_target_temperature(60) + assert water_target_temp() == 60 + + def test_set_target_temperature_wrong_input(self): + def water_target_temp(): + return self.device.status().water_target_temperature + + self.device.set_target_temperature(40) + assert water_target_temp() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_temperature(29) + assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: 29" + assert water_target_temp() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_temperature(61) + assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: 61" + assert water_target_temp() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_temperature(-10) + assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: -10" + assert water_target_temp() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_temperature("smth") + assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: smth" + assert water_target_temp() == 40 + + def test_set_target_humidity(self): + def target_humidity(): + return self.device.status().target_humidity + + self.device.set_target_humidity(0) + assert target_humidity() == 0 + + self.device.set_target_humidity(31) + assert target_humidity() == 31 + + self.device.set_target_humidity(99) + assert target_humidity() == 99 + + # self.device.set_target_humidity(160) + # assert target_humidity() == 160 + + def test_set_target_humidity_wrong_input(self): + def target_humidity(): + return self.device.status().target_humidity + + self.device.set_target_humidity(40) + assert target_humidity() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_humidity(-10) + assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: -10" + assert target_humidity() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_humidity(100) + assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: 100" + assert target_humidity() == 40 + + with pytest.raises(AirHumidifierException) as excinfo: + self.device.set_target_humidity("smth") + assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: smth" + assert target_humidity() == 40 + + # def test_status_without_temperature(self): + # self.device._reset_state() + # self.device.state["temperature"] = None + # + # assert self.state().temperature is None + # + # def test_status_without_led_brightness(self): + # self.device._reset_state() + # self.device.state["led_brightness"] = None + # + # assert self.state().led_brightness is LedBrightness.Off + # + # def test_status_without_mode(self): + # self.device._reset_state() + # self.device.state["mode"] = None + # + # assert self.state().mode is OperationMode.Intelligent + # From 805cd36c721ab807b3f99b7ac67594a76886f8e5 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sat, 5 Feb 2022 23:17:01 +0300 Subject: [PATCH 2/8] refactor: Switch Jsq001 to API used in Jsq002 tests --- miio/tests/test_airhumidifier_jsq.py | 99 +++++++++++++++------------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/miio/tests/test_airhumidifier_jsq.py b/miio/tests/test_airhumidifier_jsq.py index 5d60e520d..c95ea0b28 100644 --- a/miio/tests/test_airhumidifier_jsq.py +++ b/miio/tests/test_airhumidifier_jsq.py @@ -45,43 +45,24 @@ def __init__(self, *args, **kwargs): self.device_info = None - self.state = OrderedDict( - ( - ("temperature", 24), - ("humidity", 29), - ("mode", 3), - ("buzzer", 1), - ("child_lock", 1), - ("led_brightness", 2), - ("power", 1), - ("no_water", 1), - ("lid_opened", 1), - ) - ) - self.start_state = self.state.copy() + self.start_state = [24, 29, 3, 1, 1, 2, 1, 1, 1] + self.state = self.start_state.copy() + + # Mocks for `device.send("cmd_name", args)` commands: + self._available_properties = AVAILABLE_PROPERTIES[self._model] self.return_values = { "get_props": self._get_state, - "set_start": lambda x: self._set_state("power", x), - "set_mode": lambda x: self._set_state("mode", x), - "set_brightness": lambda x: self._set_state("led_brightness", x), - "set_buzzer": lambda x: self._set_state("buzzer", x), - "set_lock": lambda x: self._set_state("child_lock", x), + "set_start": lambda x: self._set_state_by_key("power", x), + "set_mode": lambda x: self._set_state_by_key("mode", x), + "set_brightness": lambda x: self._set_state_by_key("led_brightness", x), + "set_buzzer": lambda x: self._set_state_by_key("buzzer", x), + "set_lock": lambda x: self._set_state_by_key("child_lock", x), "miIO.info": self._get_device_info, } super().__init__(args, kwargs) - def _get_device_info(self, _): - """Return dummy device info.""" - return self.dummy_device_info - - def _get_state(self, props): - """Return wanted properties.""" - return list(self.state.values()) - - -class DummyAirHumidifierJsq002(DummyDevice, AirHumidifierJsq002): def _set_state_by_key(self, key, value): self._set_state( self._available_properties.index(key), @@ -91,6 +72,16 @@ def _set_state_by_key(self, key, value): def _get_state_by_key(self, key): return self.state[self._available_properties.index(key)] + def _get_device_info(self, _): + """Return dummy device info.""" + return self.dummy_device_info + + def _get_state(self, props): + """Mocks device `get_props` command """ + return self.state + + +class DummyAirHumidifierJsq002(DummyDevice, AirHumidifierJsq002): def __init__(self, *args, **kwargs): self._model = MODEL_HUMIDIFIER_JSQ002 @@ -122,6 +113,15 @@ def __init__(self, *args, **kwargs): super().__init__(args, kwargs) + def _set_state_by_key(self, key, value): + self._set_state( + self._available_properties.index(key), + value + ) + + def _get_state_by_key(self, key): + return self.state[self._available_properties.index(key)] + def _get_state(self, props): """Mocks device `get_props` command """ return self.state @@ -204,19 +204,22 @@ def test_off(self): def test_status(self): self.device._reset_state() - assert repr(self.state()) == repr(AirHumidifierStatus(self.device.start_state)) + state: AirHumidifierStatus = self.state() + properties = AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_JSQ001] + assert repr(state) == repr(AirHumidifierStatus({ + k: v for k, v in zip(properties, self.device.start_state) + })) - assert self.state().temperature == self.device.start_state["temperature"] - assert self.state().humidity == self.device.start_state["humidity"] - assert self.state().mode == OperationMode(self.device.start_state["mode"]) - assert self.state().buzzer == (self.device.start_state["buzzer"] == 1) - assert self.state().child_lock == (self.device.start_state["child_lock"] == 1) - assert self.state().led_brightness == LedBrightness( - self.device.start_state["led_brightness"] - ) + assert state.data['temperature'] == 24 + assert state.data['humidity'] == 29 + assert state.data['mode'] == 3 + assert state.data['buzzer'] == 1 + assert state.data['child_lock'] == 1 + assert state.data['led_brightness'] == 2 assert self._is_on() is True - assert self.state().no_water == (self.device.start_state["no_water"] == 1) - assert self.state().lid_opened == (self.device.start_state["lid_opened"] == 1) + assert state.data['power'] == 1 + assert state.data['no_water'] == 1 + assert state.data['lid_opened'] == 1 def test_status_wrong_input(self): def mode(): @@ -227,16 +230,16 @@ def led_brightness(): self.device._reset_state() - self.device.state["mode"] = 10 + self.device._set_state_by_key("mode", [10]) assert mode() == OperationMode.Intelligent - self.device.state["mode"] = "smth" + self.device._set_state_by_key("mode", ["smt"]) assert mode() == OperationMode.Intelligent - self.device.state["led_brightness"] = 10 + self.device._set_state_by_key("led_brightness", [10]) assert led_brightness() == LedBrightness.Off - self.device.state["led_brightness"] = "smth" + self.device._set_state_by_key("led_brightness", ["smt"]) assert led_brightness() == LedBrightness.Off def test_set_mode(self): @@ -324,19 +327,21 @@ def test_set_buzzer(self): def test_status_without_temperature(self): self.device._reset_state() - self.device.state["temperature"] = None + self.device._set_state_by_key("temperature", [None]) assert self.state().temperature is None def test_status_without_led_brightness(self): self.device._reset_state() - self.device.state["led_brightness"] = None + + self.device._set_state_by_key("led_brightness", [None]) assert self.state().led_brightness is LedBrightness.Off def test_status_without_mode(self): self.device._reset_state() - self.device.state["mode"] = None + + self.device._set_state_by_key("mode", [None]) assert self.state().mode is OperationMode.Intelligent From 15cf634e15ff41c1eec3e72f44805c58f1d7f2cc Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 00:40:28 +0300 Subject: [PATCH 3/8] refactor: code cleanup --- miio/__init__.py | 3 +- miio/airhumidifier_jsq.py | 98 ++++++++-------- miio/discovery.py | 4 +- miio/tests/test_airhumidifier_jsq.py | 161 +++++++++++++-------------- 4 files changed, 138 insertions(+), 128 deletions(-) diff --git a/miio/__init__.py b/miio/__init__.py index 8b37ca4e3..111cc074e 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -24,8 +24,7 @@ from miio.airfresh import AirFresh, AirFreshVA4 from miio.airfresh_t2017 import AirFreshA1, AirFreshT2017 from miio.airhumidifier import AirHumidifier, AirHumidifierCA1, AirHumidifierCB1 -from miio.airhumidifier_jsq import AirHumidifierJsq -from miio.airhumidifier_jsq import AirHumidifierJsq002 +from miio.airhumidifier_jsq import AirHumidifierJsq, AirHumidifierJsq002 from miio.airhumidifier_miot import AirHumidifierMiot from miio.airhumidifier_mjjsq import AirHumidifierMjjsq from miio.airpurifier import AirPurifier diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index 304cafc21..e110b38f9 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -17,14 +17,8 @@ # Array of several common commands SHARED_COMMANDS = { - MODEL_HUMIDIFIER_JSQ001: dict( - power_on_off="set_start", - buzzer_on_off="set_buzzer" - ), - MODEL_HUMIDIFIER_JSQ002: dict( - power_on_off="on_off", - buzzer_on_off="buzzer_on" - ) + MODEL_HUMIDIFIER_JSQ001: dict(power_on_off="set_start", buzzer_on_off="set_buzzer"), + MODEL_HUMIDIFIER_JSQ002: dict(power_on_off="on_off", buzzer_on_off="buzzer_on"), } # Array of properties in same order as in humidifier response @@ -54,7 +48,6 @@ # Properties: # > dev.send("get_props","") # [1, 2, 36, 2, 46, 4, 1, 1, 1, 50, 51, 0] - # res[0]=1 Values (0: off/sleep, 1: on); "power", # CMD: on_off [int] # res[1]=2 Values (1, 2, 3); fan speed in UI: {Gear: level 1, level 2, level 3}; @@ -70,16 +63,16 @@ # res[6]=1, Water heater values (0: off, 1: on) "heat", # CMD: warm_on [int] # res[7]=1 BeaPower values (0: off, 1: on) - 'buzzer', # CMD: buzzer_on [int] + "buzzer", # CMD: buzzer_on [int] # res[8]=1, Values (0: off, 1: on) - 'child_lock', # CMD: set_lock [int] + "child_lock", # CMD: set_lock [int] # res[9]=50 Values (water heater target temp in degrees, [int]: 30..60) - 'target_temperature', # CMD: set_temp [int] + "target_temperature", # CMD: set_temp [int] # res[10]=51 Values (% [int]) - 'target_humidity', # CMD: set_humidity [int] + "target_humidity", # CMD: set_humidity [int] # res[11]=0 Values: We failed to find when it changes, is not lid opened event - 'reserved', # XXX: cmd rst_clean [] ? - ] + "reserved", # XXX: cmd rst_clean [] ? + ], } @@ -110,6 +103,10 @@ class LedBrightnessJsq002(enum.Enum): class AirHumidifierStatusJsqCommon(DeviceStatus): + def __init__(self, data: Dict[str, Any]) -> None: + """Status of an Air Humidifier:""" + self.data = data + @property def power(self) -> str: """Power state.""" @@ -144,15 +141,10 @@ def child_lock(self) -> bool: class AirHumidifierStatus(AirHumidifierStatusJsqCommon): """Container for status reports from the air humidifier jsq.""" - def __init__(self, data: Dict[str, Any]) -> None: - """ - Status of an Air Humidifier (shuii.humidifier.jsq001): - """ - self.data = data - @property def mode(self) -> OperationMode: """Operation mode. + Can be either low, medium, high or humidity. """ @@ -200,12 +192,6 @@ def use_time(self) -> Optional[int]: class AirHumidifierStatusJsq002(AirHumidifierStatusJsqCommon): - def __init__(self, data: Dict[str, Any]) -> None: - """ - Status of Xiaomi 'Zero Fog DWZF(G)-4500Z` (shuii.humidifier.jsq002): - """ - self.data = data - @property def mode(self) -> OperationModeJsq002: """Operation mode. @@ -232,11 +218,13 @@ def temperature(self) -> int: @property def water_level(self) -> int: - """Water level: 0,1,2,3,4,5""" + """Water level: 0,1,2,3,4,5.""" level = self.data["water_level"] if level not in [0, 1, 2, 3, 4, 5]: - _LOGGER.exception("Water level should be 0,1,2,3,4,5. But was: %s", str(level)) + _LOGGER.exception( + "Water level should be 0,1,2,3,4,5. But was: %s", str(level) + ) return 5 return level @@ -252,7 +240,10 @@ def water_target_temperature(self) -> int: target_temp = self.data["target_temperature"] target_temp_int = int(target_temp) if target_temp_int not in range(30, 61): - _LOGGER.exception("Target water heater temp should be in [30..60]. But was: %s", str(target_temp)) + _LOGGER.exception( + "Target water heater temp should be in [30..60]. But was: %s", + str(target_temp), + ) return 30 return target_temp_int @@ -263,7 +254,10 @@ def target_humidity(self) -> int: target_humidity = self.data["target_humidity"] target_humidity_int = int(target_humidity) if target_humidity_int not in range(0, 100): - _LOGGER.exception("Target humidity should be in [0..99]. But was: %s", str(target_humidity)) + _LOGGER.exception( + "Target humidity should be in [0..99]. But was: %s", + str(target_humidity), + ) return 30 return target_humidity_int @@ -316,7 +310,7 @@ def on(self): if self.model not in SHARED_COMMANDS: raise AirHumidifierException("Unsupported model: %s" % self.model) - return self.send(SHARED_COMMANDS[self.model]['power_on_off'], [1]) + return self.send(SHARED_COMMANDS[self.model]["power_on_off"], [1]) @command(default_output=format_output("Powering off")) def off(self): @@ -324,7 +318,7 @@ def off(self): if self.model not in SHARED_COMMANDS: raise AirHumidifierException("Unsupported model: %s" % self.model) - return self.send(SHARED_COMMANDS[self.model]['power_on_off'], [0]) + return self.send(SHARED_COMMANDS[self.model]["power_on_off"], [0]) @command( click.argument("buzzer", type=bool), @@ -338,8 +332,7 @@ def set_buzzer(self, buzzer: bool): raise AirHumidifierException("Unsupported model: %s" % self.model) return self.send( - SHARED_COMMANDS[self.model]['buzzer_on_off'], - [int(bool(buzzer))] + SHARED_COMMANDS[self.model]["buzzer_on_off"], [int(bool(buzzer))] ) @command( @@ -423,7 +416,7 @@ class AirHumidifierJsq002(AirHumidifierJsqCommon): "Water level: {result.water_level}\n" "Water heater: {result.heater}\n" "Water target temperature: {result.water_target_temperature} °C\n" - "Target humidity: {result.target_humidity} %\n" + "Target humidity: {result.target_humidity} %\n", ) ) def status(self) -> AirHumidifierStatusJsq002: @@ -438,7 +431,7 @@ def set_mode(self, mode: OperationModeJsq002): value = mode.value if value not in (om.value for om in OperationModeJsq002): raise AirHumidifierException( - "{} is not a valid OperationModeJsq2 value".format(value) + f"{value} is not a valid OperationModeJsq2 value" ) return self.send("set_gear", [value]) @@ -452,7 +445,7 @@ def set_led_brightness(self, brightness: LedBrightnessJsq002): value = brightness.value if value not in (lb.value for lb in LedBrightnessJsq002): raise AirHumidifierException( - "{} is not a valid LedBrightnessJsq2 value".format(value) + f"{value} is not a valid LedBrightnessJsq2 value" ) return self.send("set_led", [value]) @@ -469,27 +462,44 @@ def set_led(self, led: bool): return self.set_led_brightness(brightness) @command( - click.argument("lock", type=bool), + click.argument("heater", type=bool), default_output=format_output( - lambda lock: "Turning on child lock" if lock else "Turning off child lock" + lambda heater: "Turning on water heater" + if heater + else "Turning off water heater" ), ) def set_heater(self, heater_on: bool): """Set water heater on/off.""" return self.send("warm_on", [int(bool(heater_on))]) - def set_target_temperature(self, temperature: int): - """Set the target temperature degrees C, only 30..60.""" + @command( + click.argument("temperature", type=int), + default_output=format_output( + "Setting target water temperature to {temperature}" + ), + ) + def set_target_water_temperature(self, temperature: int): + """Set the target water temperature degrees C, only 30..60.""" if temperature not in range(30, 61): - raise AirHumidifierException("Invalid water target temperature, should be in [30..60]. But was: %s" % temperature) + raise AirHumidifierException( + "Invalid water target temperature, should be in [30..60]. But was: %s" + % temperature + ) return self.send("set_temp", [temperature]) + @command( + click.argument("humidity", type=int), + default_output=format_output("Setting target humidity to {humidity}"), + ) def set_target_humidity(self, humidity: int): """Set the target humidity %, only 0..99.""" if humidity not in range(0, 100): - raise AirHumidifierException("Invalid target humidity, should be in [0..99]. But was: %s" % humidity) + raise AirHumidifierException( + "Invalid target humidity, should be in [0..99]. But was: %s" % humidity + ) return self.send("set_humidity", [humidity]) diff --git a/miio/discovery.py b/miio/discovery.py index 274eeb6dd..91cba7484 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -136,7 +136,9 @@ "zhimi-humidifier-ca1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CA1), "zhimi-humidifier-cb1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_CB1), "shuii-humidifier-jsq001": partial(AirHumidifierJsq, model=MODEL_HUMIDIFIER_JSQ001), - "shuii-humidifier-jsq002": partial(AirHumidifierJsq002, model=MODEL_HUMIDIFIER_JSQ002), + "shuii-humidifier-jsq002": partial( + AirHumidifierJsq002, model=MODEL_HUMIDIFIER_JSQ002 + ), "deerma-humidifier-mjjsq": partial( AirHumidifierMjjsq, model=MODEL_HUMIDIFIER_MJJSQ ), diff --git a/miio/tests/test_airhumidifier_jsq.py b/miio/tests/test_airhumidifier_jsq.py index c95ea0b28..bc6a5e3cb 100644 --- a/miio/tests/test_airhumidifier_jsq.py +++ b/miio/tests/test_airhumidifier_jsq.py @@ -1,20 +1,19 @@ -from collections import OrderedDict from unittest import TestCase import pytest -from miio import AirHumidifierJsq,AirHumidifierJsq002 +from miio import AirHumidifierJsq, AirHumidifierJsq002 from miio.airhumidifier import AirHumidifierException from miio.airhumidifier_jsq import ( AVAILABLE_PROPERTIES, MODEL_HUMIDIFIER_JSQ001, + MODEL_HUMIDIFIER_JSQ002, AirHumidifierStatus, + AirHumidifierStatusJsq002, LedBrightness, + LedBrightnessJsq002, OperationMode, - MODEL_HUMIDIFIER_JSQ002, - AirHumidifierStatusJsq002, OperationModeJsq002, - LedBrightnessJsq002 ) from .dummies import DummyDevice @@ -53,7 +52,7 @@ def __init__(self, *args, **kwargs): self.return_values = { "get_props": self._get_state, - "set_start": lambda x: self._set_state_by_key("power", x), + "set_start": lambda x: self._set_state_by_key("power", x), "set_mode": lambda x: self._set_state_by_key("mode", x), "set_brightness": lambda x: self._set_state_by_key("led_brightness", x), "set_buzzer": lambda x: self._set_state_by_key("buzzer", x), @@ -64,10 +63,7 @@ def __init__(self, *args, **kwargs): super().__init__(args, kwargs) def _set_state_by_key(self, key, value): - self._set_state( - self._available_properties.index(key), - value - ) + self._set_state(self._available_properties.index(key), value) def _get_state_by_key(self, key): return self.state[self._available_properties.index(key)] @@ -77,7 +73,7 @@ def _get_device_info(self, _): return self.dummy_device_info def _get_state(self, props): - """Mocks device `get_props` command """ + """Mocks device `get_props` command.""" return self.state @@ -114,16 +110,13 @@ def __init__(self, *args, **kwargs): super().__init__(args, kwargs) def _set_state_by_key(self, key, value): - self._set_state( - self._available_properties.index(key), - value - ) + self._set_state(self._available_properties.index(key), value) def _get_state_by_key(self, key): return self.state[self._available_properties.index(key)] def _get_state(self, props): - """Mocks device `get_props` command """ + """Mocks device `get_props` command.""" return self.state def _get_device_info(self, _): @@ -206,20 +199,22 @@ def test_status(self): state: AirHumidifierStatus = self.state() properties = AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_JSQ001] - assert repr(state) == repr(AirHumidifierStatus({ - k: v for k, v in zip(properties, self.device.start_state) - })) - - assert state.data['temperature'] == 24 - assert state.data['humidity'] == 29 - assert state.data['mode'] == 3 - assert state.data['buzzer'] == 1 - assert state.data['child_lock'] == 1 - assert state.data['led_brightness'] == 2 + assert repr(state) == repr( + AirHumidifierStatus( + {k: v for k, v in zip(properties, self.device.start_state)} + ) + ) + + assert state.data["temperature"] == 24 + assert state.data["humidity"] == 29 + assert state.data["mode"] == 3 + assert state.data["buzzer"] == 1 + assert state.data["child_lock"] == 1 + assert state.data["led_brightness"] == 2 assert self._is_on() is True - assert state.data['power'] == 1 - assert state.data['no_water'] == 1 - assert state.data['lid_opened'] == 1 + assert state.data["power"] == 1 + assert state.data["no_water"] == 1 + assert state.data["lid_opened"] == 1 def test_status_wrong_input(self): def mode(): @@ -356,44 +351,46 @@ def test_status(self): state: AirHumidifierStatusJsq002 = self.state() properties = AVAILABLE_PROPERTIES[MODEL_HUMIDIFIER_JSQ002] - assert repr(state) == repr(AirHumidifierStatusJsq002({ - k: v for k, v in zip(properties, self.device.start_state) - })) + assert repr(state) == repr( + AirHumidifierStatusJsq002( + {k: v for k, v in zip(properties, self.device.start_state)} + ) + ) - assert state.data['power'] == 1 + assert state.data["power"] == 1 assert self._is_on() is True - assert state.data['mode'] == 2 + assert state.data["mode"] == 2 assert state.mode == OperationModeJsq002.Level2 - assert state.data['humidity'] == 36 + assert state.data["humidity"] == 36 assert state.humidity == 36 - assert state.data['led_brightness'] == 2 + assert state.data["led_brightness"] == 2 assert state.led_brightness == LedBrightnessJsq002.Low - assert state.data['temperature'] == 46 + assert state.data["temperature"] == 46 assert state.temperature == 26 - assert state.data['water_level'] == 4 + assert state.data["water_level"] == 4 assert state.water_level == 4 - assert state.data['heat'] == 1 + assert state.data["heat"] == 1 assert state.heater is True - assert state.data['buzzer'] == 1 + assert state.data["buzzer"] == 1 assert state.buzzer is True - assert state.data['child_lock'] == 1 + assert state.data["child_lock"] == 1 assert state.child_lock is True - assert state.data['target_temperature'] == 50 + assert state.data["target_temperature"] == 50 assert state.water_target_temperature == 50 - assert state.data['target_humidity'] == 51 + assert state.data["target_humidity"] == 51 assert state.target_humidity == 51 - assert state.data['reserved'] == 0 + assert state.data["reserved"] == 0 def test_on(self): self.device.off() # ensure off @@ -553,47 +550,59 @@ def test_set_buzzer(self): def test_set_child_lock(self): self.do_test_on_off_property("set_child_lock", "child_lock") - def test_set_target_temperature(self): + def test_set_target_water_temperature(self): def water_target_temp(): return self.device.status().water_target_temperature - self.device.set_target_temperature(30) + self.device.set_target_water_temperature(30) assert water_target_temp() == 30 - self.device.set_target_temperature(31) + self.device.set_target_water_temperature(31) assert water_target_temp() == 31 - self.device.set_target_temperature(59) + self.device.set_target_water_temperature(59) assert water_target_temp() == 59 - self.device.set_target_temperature(60) + self.device.set_target_water_temperature(60) assert water_target_temp() == 60 - def test_set_target_temperature_wrong_input(self): + def test_set_target_water_temperature_wrong_input(self): def water_target_temp(): return self.device.status().water_target_temperature - self.device.set_target_temperature(40) + self.device.set_target_water_temperature(40) assert water_target_temp() == 40 with pytest.raises(AirHumidifierException) as excinfo: - self.device.set_target_temperature(29) - assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: 29" + self.device.set_target_water_temperature(29) + assert ( + str(excinfo.value) + == "Invalid water target temperature, should be in [30..60]. But was: 29" + ) assert water_target_temp() == 40 with pytest.raises(AirHumidifierException) as excinfo: - self.device.set_target_temperature(61) - assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: 61" + self.device.set_target_water_temperature(61) + assert ( + str(excinfo.value) + == "Invalid water target temperature, should be in [30..60]. But was: 61" + ) assert water_target_temp() == 40 with pytest.raises(AirHumidifierException) as excinfo: - self.device.set_target_temperature(-10) - assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: -10" + self.device.set_target_water_temperature(-10) + assert ( + str(excinfo.value) + == "Invalid water target temperature, should be in [30..60]. But was: -10" + ) assert water_target_temp() == 40 with pytest.raises(AirHumidifierException) as excinfo: - self.device.set_target_temperature("smth") - assert str(excinfo.value) == "Invalid water target temperature, should be in [30..60]. But was: smth" + self.device.set_target_water_temperature("smth") + assert ( + str(excinfo.value) + == "Invalid water target temperature, should be in [30..60]. But was: smth" + ) assert water_target_temp() == 40 def test_set_target_humidity(self): @@ -621,34 +630,24 @@ def target_humidity(): with pytest.raises(AirHumidifierException) as excinfo: self.device.set_target_humidity(-10) - assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: -10" + assert ( + str(excinfo.value) + == "Invalid target humidity, should be in [0..99]. But was: -10" + ) assert target_humidity() == 40 with pytest.raises(AirHumidifierException) as excinfo: self.device.set_target_humidity(100) - assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: 100" + assert ( + str(excinfo.value) + == "Invalid target humidity, should be in [0..99]. But was: 100" + ) assert target_humidity() == 40 with pytest.raises(AirHumidifierException) as excinfo: self.device.set_target_humidity("smth") - assert str(excinfo.value) == "Invalid target humidity, should be in [0..99]. But was: smth" + assert ( + str(excinfo.value) + == "Invalid target humidity, should be in [0..99]. But was: smth" + ) assert target_humidity() == 40 - - # def test_status_without_temperature(self): - # self.device._reset_state() - # self.device.state["temperature"] = None - # - # assert self.state().temperature is None - # - # def test_status_without_led_brightness(self): - # self.device._reset_state() - # self.device.state["led_brightness"] = None - # - # assert self.state().led_brightness is LedBrightness.Off - # - # def test_status_without_mode(self): - # self.device._reset_state() - # self.device.state["mode"] = None - # - # assert self.state().mode is OperationMode.Intelligent - # From d62a1e01ecb75b39a56ae3bfb7a61eb7b7649832 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 18:10:15 +0300 Subject: [PATCH 4/8] docs: CLI documentation updated --- miio/airhumidifier_jsq.py | 50 ++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index e110b38f9..ce08f656e 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -327,7 +327,10 @@ def off(self): ), ) def set_buzzer(self, buzzer: bool): - """Set buzzer on/off.""" + """Set buzzer on/off. + + Supported args one of: true, false, 0, 1 + """ if self.model not in SHARED_COMMANDS: raise AirHumidifierException("Unsupported model: %s" % self.model) @@ -342,7 +345,10 @@ def set_buzzer(self, buzzer: bool): ), ) def set_child_lock(self, lock: bool): - """Set child lock on/off.""" + """Set child lock on/off. + + Supported args one of: true, false, 0, 1 + """ return self.send("set_lock", [int(bool(lock))]) @@ -371,7 +377,10 @@ def status(self) -> AirHumidifierStatus: default_output=format_output("Setting mode to '{mode.value}'"), ) def set_mode(self, mode: OperationMode): - """Set mode.""" + """Set mode. + + Supported args one of: 'intelligent', 'level1', 'level2', 'level3', 'level4' + """ value = mode.value if value not in (om.value for om in OperationMode): raise AirHumidifierException(f"{value} is not a valid OperationMode value") @@ -383,7 +392,10 @@ def set_mode(self, mode: OperationMode): default_output=format_output("Setting LED brightness to {brightness}"), ) def set_led_brightness(self, brightness: LedBrightness): - """Set led brightness.""" + """Set led brightness. + + Supported args one of: 'high', 'low', 'off'. + """ value = brightness.value if value not in (lb.value for lb in LedBrightness): raise AirHumidifierException(f"{value} is not a valid LedBrightness value") @@ -397,7 +409,10 @@ def set_led_brightness(self, brightness: LedBrightness): ), ) def set_led(self, led: bool): - """Turn led on/off.""" + """Turn led on/off. + + Supported args one of: true, false, 0, 1 + """ brightness = LedBrightness.High if led else LedBrightness.Off return self.set_led_brightness(brightness) @@ -427,7 +442,10 @@ def status(self) -> AirHumidifierStatusJsq002: default_output=format_output("Setting mode to '{mode.value}'"), ) def set_mode(self, mode: OperationModeJsq002): - """Set mode.""" + """Set mode. + + Supported args one of: 'level1', 'level2', 'level3' + """ value = mode.value if value not in (om.value for om in OperationModeJsq002): raise AirHumidifierException( @@ -441,7 +459,10 @@ def set_mode(self, mode: OperationModeJsq002): default_output=format_output("Setting LED brightness to {brightness}"), ) def set_led_brightness(self, brightness: LedBrightnessJsq002): - """Set led brightness.""" + """Set led brightness. + + Supported args one of: 'high', 'low', 'off'. + """ value = brightness.value if value not in (lb.value for lb in LedBrightnessJsq002): raise AirHumidifierException( @@ -457,7 +478,10 @@ def set_led_brightness(self, brightness: LedBrightnessJsq002): ), ) def set_led(self, led: bool): - """Turn led on/off.""" + """Turn led on/off. + + Supported args one of: true, false, 0, 1 + """ brightness = LedBrightnessJsq002.High if led else LedBrightnessJsq002.Off return self.set_led_brightness(brightness) @@ -470,7 +494,10 @@ def set_led(self, led: bool): ), ) def set_heater(self, heater_on: bool): - """Set water heater on/off.""" + """Set water heater on/off. + + Supported args one of: true, false, 0, 1 + """ return self.send("warm_on", [int(bool(heater_on))]) @command( @@ -480,7 +507,8 @@ def set_heater(self, heater_on: bool): ), ) def set_target_water_temperature(self, temperature: int): - """Set the target water temperature degrees C, only 30..60.""" + """Set the target water temperature degrees C, supported integer numbers in + range 30..60.""" if temperature not in range(30, 61): raise AirHumidifierException( @@ -495,7 +523,7 @@ def set_target_water_temperature(self, temperature: int): default_output=format_output("Setting target humidity to {humidity}"), ) def set_target_humidity(self, humidity: int): - """Set the target humidity %, only 0..99.""" + """Set the target humidity %, supported integer numbers in range 0..99.""" if humidity not in range(0, 100): raise AirHumidifierException( From b515b23d57ca7923e172ac739dc2f5e1d3d1b475 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 20:06:00 +0300 Subject: [PATCH 5/8] fix: set_heater fixed --- miio/airhumidifier_jsq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index ce08f656e..40e5b5be7 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -493,12 +493,12 @@ def set_led(self, led: bool): else "Turning off water heater" ), ) - def set_heater(self, heater_on: bool): + def set_heater(self, heater: bool): """Set water heater on/off. Supported args one of: true, false, 0, 1 """ - return self.send("warm_on", [int(bool(heater_on))]) + return self.send("warm_on", [int(bool(heater))]) @command( click.argument("temperature", type=int), From d59462eeaea03e866fdd62bfeacc71330fbd4d63 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 20:19:11 +0300 Subject: [PATCH 6/8] feature: support 2 more commands --- miio/airhumidifier_jsq.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index 40e5b5be7..bde9adb2e 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -71,7 +71,7 @@ # res[10]=51 Values (% [int]) "target_humidity", # CMD: set_humidity [int] # res[11]=0 Values: We failed to find when it changes, is not lid opened event - "reserved", # XXX: cmd rst_clean [] ? + "reserved", ], } @@ -531,3 +531,17 @@ def set_target_humidity(self, humidity: int): ) return self.send("set_humidity", [humidity]) + + @command( + default_output=format_output("Running `rst_clean` command"), + ) + def rst_clean(self): + """Run 'rst_clean' command (unknown function).""" + return self.send("rst_clean", []) + + @command( + default_output=format_output("Calibrating water level as zero"), + ) + def corrected_water(self): + """Calibrate current water level as zero level.""" + return self.send("corrected_water", []) From 7837a04d4b1e0ffe6cf2f105ea69569be685cd2f Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 20:22:09 +0300 Subject: [PATCH 7/8] docs: water level info updated --- miio/airhumidifier_jsq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index bde9adb2e..4edd5e781 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -58,7 +58,7 @@ "led_brightness", # CMD: set_led [int] # res[4]=26 Values(is "ambient temp degrees int"+20, i.e. 46 corresponds to 26); "temperature", - # res[5]=4 (0,1,2,3,4,5) + # res[5]=4 (0,1,2,3,4,5). '5' is full, if pour more water, warning signal will sound "water_level", # Get cmd: corrected_water [] # res[6]=1, Water heater values (0: off, 1: on) "heat", # CMD: warm_on [int] From c2b157483f44aaed8733b4b14b5879a126400636 Mon Sep 17 00:00:00 2001 From: Roman Chernyatchik Date: Sun, 6 Feb 2022 21:44:12 +0300 Subject: [PATCH 8/8] refactor: use better property name --- miio/airhumidifier_jsq.py | 6 ++++-- miio/tests/test_airhumidifier_jsq.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/miio/airhumidifier_jsq.py b/miio/airhumidifier_jsq.py index 4edd5e781..737cb843c 100644 --- a/miio/airhumidifier_jsq.py +++ b/miio/airhumidifier_jsq.py @@ -235,7 +235,7 @@ def heater(self) -> bool: return self.data["heat"] == 1 @property - def water_target_temperature(self) -> int: + def target_water_temperature(self) -> int: """Return Target Water Temperature, degrees C, 30..60.""" target_temp = self.data["target_temperature"] target_temp_int = int(target_temp) @@ -418,6 +418,8 @@ def set_led(self, led: bool): class AirHumidifierJsq002(AirHumidifierJsqCommon): + """Implementation of Xiaomi Zero Fog DWZF(G)-4500Z: shuii.humidifier.jsq002.""" + @command( default_output=format_output( "", @@ -430,7 +432,7 @@ class AirHumidifierJsq002(AirHumidifierJsqCommon): "Child lock: {result.child_lock}\n" "Water level: {result.water_level}\n" "Water heater: {result.heater}\n" - "Water target temperature: {result.water_target_temperature} °C\n" + "Water target temperature: {result.target_water_temperature} °C\n" "Target humidity: {result.target_humidity} %\n", ) ) diff --git a/miio/tests/test_airhumidifier_jsq.py b/miio/tests/test_airhumidifier_jsq.py index bc6a5e3cb..d048f4f2b 100644 --- a/miio/tests/test_airhumidifier_jsq.py +++ b/miio/tests/test_airhumidifier_jsq.py @@ -385,7 +385,7 @@ def test_status(self): assert state.child_lock is True assert state.data["target_temperature"] == 50 - assert state.water_target_temperature == 50 + assert state.target_water_temperature == 50 assert state.data["target_humidity"] == 51 assert state.target_humidity == 51 @@ -552,7 +552,7 @@ def test_set_child_lock(self): def test_set_target_water_temperature(self): def water_target_temp(): - return self.device.status().water_target_temperature + return self.device.status().target_water_temperature self.device.set_target_water_temperature(30) assert water_target_temp() == 30 @@ -568,7 +568,7 @@ def water_target_temp(): def test_set_target_water_temperature_wrong_input(self): def water_target_temp(): - return self.device.status().water_target_temperature + return self.device.status().target_water_temperature self.device.set_target_water_temperature(40) assert water_target_temp() == 40