diff --git a/miio/__init__.py b/miio/__init__.py index 0ad453210..6fd9f4e64 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -90,7 +90,11 @@ from miio.integrations.zhimi.airpurifier import AirFresh, AirPurifier, AirPurifierMiot from miio.integrations.zhimi.fan import Fan, FanZA5 from miio.integrations.zhimi.heater import Heater, HeaterMiot -from miio.integrations.zhimi.humidifier import AirHumidifier, AirHumidifierMiot +from miio.integrations.zhimi.humidifier import ( + AirHumidifier, + AirHumidifierMiot, + AirHumidifierMiotCA6, +) from miio.integrations.zimi.powerstrip import PowerStrip from miio.protocol import Message, Utils from miio.push_server import EventInfo, PushServer diff --git a/miio/integrations/zhimi/humidifier/__init__.py b/miio/integrations/zhimi/humidifier/__init__.py index 26b999c4f..b56912279 100644 --- a/miio/integrations/zhimi/humidifier/__init__.py +++ b/miio/integrations/zhimi/humidifier/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa from .airhumidifier import AirHumidifier -from .airhumidifier_miot import AirHumidifierMiot +from .airhumidifier_miot import AirHumidifierMiot, AirHumidifierMiotCA6 diff --git a/miio/integrations/zhimi/humidifier/airhumidifier_miot.py b/miio/integrations/zhimi/humidifier/airhumidifier_miot.py index e0543aa96..5cf4f49c3 100644 --- a/miio/integrations/zhimi/humidifier/airhumidifier_miot.py +++ b/miio/integrations/zhimi/humidifier/airhumidifier_miot.py @@ -6,14 +6,16 @@ from miio import DeviceStatus, MiotDevice from miio.click_common import EnumType, command, format_output +from miio.devicestatus import sensor, setting _LOGGER = logging.getLogger(__name__) SMARTMI_EVAPORATIVE_HUMIDIFIER_2 = "zhimi.humidifier.ca4" +SMARTMI_EVAPORATIVE_HUMIDIFIER_3 = "zhimi.humidifier.ca6" -_MAPPINGS = { +_MAPPINGS_CA4 = { SMARTMI_EVAPORATIVE_HUMIDIFIER_2: { # Source http://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca4:2 # Air Humidifier (siid=2) @@ -44,6 +46,49 @@ } +_MAPPINGS_CA6 = { + SMARTMI_EVAPORATIVE_HUMIDIFIER_3: { + # Source https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca6:1 + # Air Humidifier (siid=2) + "power": {"siid": 2, "piid": 1}, # bool + "fault": {"siid": 2, "piid": 2}, # [0, 15] step 1 + "mode": { + "siid": 2, + "piid": 5, + }, # 0 - Fav, 1 - Auto, 2 - Sleep + "target_humidity": { + "siid": 2, + "piid": 6, + }, # [30, 60] step 1 + "water_level": { + "siid": 2, + "piid": 7, + }, # 0 - empty/min, 1 - normal, 2 - full/max + "dry": {"siid": 2, "piid": 8}, # Automatic Air Drying, bool + "status": {"siid": 2, "piid": 9}, # 1 - Close, 2 - Work, 3 - Dry, 4 - Clean + # Environment (siid=3) + "temperature": {"siid": 3, "piid": 7}, # [-40, 125] step 0.1 + "humidity": {"siid": 3, "piid": 9}, # [0, 100] step 1 + # Alarm (siid=4) + "buzzer": {"siid": 4, "piid": 1}, + # Indicator Light (siid=5) + "led_brightness": {"siid": 5, "piid": 2}, # 0 - Off, 1 - Dim, 2 - Brightest + # Physical Control Locked (siid=6) + "child_lock": {"siid": 6, "piid": 1}, # bool + # Other (siid=7) + "actual_speed": {"siid": 7, "piid": 1}, # [0, 2000] step 1 + "country_code": { + "siid": 7, + "piid": 4, + }, # 82 - KR, 44 - EU, 81 - JP, 86 - CN, 886 - TW + "clean_mode": {"siid": 7, "piid": 5}, # bool + "self_clean_percent": {"siid": 7, "piid": 6}, # minutes, [0, 30] step 1 + "pump_state": {"siid": 7, "piid": 7}, # bool + "pump_cnt": {"siid": 7, "piid": 8}, # [0, 4000] step 1 + } +} + + class OperationMode(enum.Enum): Auto = 0 Low = 1 @@ -51,6 +96,19 @@ class OperationMode(enum.Enum): High = 3 +class OperationModeCA6(enum.Enum): + Fav = 0 + Auto = 1 + Sleep = 2 + + +class OperationStatusCA6(enum.Enum): + Close = 1 + Work = 2 + Dry = 3 + Clean = 4 + + class LedBrightness(enum.Enum): Off = 0 Dim = 1 @@ -63,7 +121,147 @@ class PressedButton(enum.Enum): Power = 2 -class AirHumidifierMiotStatus(DeviceStatus): +class AirHumidifierMiotCommonStatus(DeviceStatus): + """Container for status reports from the air humidifier. Common features for CA4 and CA6 models.""" + + def __init__(self, data: Dict[str, Any]) -> None: + self.data = data + _LOGGER.debug( + "Status Common: %s, __cli_output__ %s", repr(self), self.__cli_output__ + ) + + # 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 target_humidity(self) -> int: + """Return target humidity.""" + return self.data["target_humidity"] + + @property + @setting( + name="Dry Mode", + icon="mdi:hair-dryer", + setter_name="set_dry", + device_class="switch", + entity_category="config", + ) + def dry(self) -> Optional[bool]: + """Return True if dry mode is on.""" + if self.data["dry"] is not None: + return self.data["dry"] + return None + + @property + @setting( + name="Clean Mode", + icon="mdi:shimmer", + setter_name="set_clean_mode", + device_class="switch", + entity_category="config", + ) + def clean_mode(self) -> bool: + """Return True if clean mode is active.""" + return self.data["clean_mode"] + + # Environment + + @property + @sensor("Humidity", unit="%", device_class="humidity") + def humidity(self) -> int: + """Return current humidity.""" + return self.data["humidity"] + + @property + @sensor("Temperature", unit="°C", device_class="temperature") + 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 + @setting( + name="Buzzer", + icon="mdi:volume-high", + setter_name="set_buzzer", + device_class="switch", + entity_category="config", + ) + 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 + @setting( + name="Led Brightness", + icon="mdi:brightness-6", + setter_name="set_led_brightness", + choices=LedBrightness, + entity_category="config", + ) + def led_brightness(self) -> Optional[LedBrightness]: + """Return brightness of the LED.""" + + if self.data["led_brightness"] is not None: + try: + return LedBrightness(self.data["led_brightness"]) + except ValueError as e: + _LOGGER.exception("Cannot parse led_brightness: %s", e) + return None + + return None + + # Physical Control Locked + + @property + @setting( + name="Child Lock", + icon="mdi:lock", + setter_name="set_child_lock", + device_class="switch", + entity_category="config", + ) + def child_lock(self) -> bool: + """Return True if child lock is on.""" + return self.data["child_lock"] + + # Other + + @property + @sensor( + "Actual Motor Speed", + unit="rpm", + device_class="measurement", + icon="mdi:fast-forward", + entity_category="diagnostic", + ) + def actual_speed(self) -> int: + """Return real speed of the motor.""" + return self.data["actual_speed"] + + +class AirHumidifierMiotStatus(AirHumidifierMiotCommonStatus): """Container for status reports from the air humidifier. Xiaomi Smartmi Evaporation Air Humidifier 2 (zhimi.humidifier.ca4) respone (MIoT format):: @@ -92,25 +290,16 @@ class AirHumidifierMiotStatus(DeviceStatus): def __init__(self, data: Dict[str, Any]) -> None: self.data = data + super().__init__(self.data) + self.embed("common", AirHumidifierMiotCommonStatus(self.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 + @setting( + name="Operation Mode", + setter_name="set_mode", + ) def mode(self) -> OperationMode: """Return current operation mode.""" @@ -123,11 +312,13 @@ def mode(self) -> OperationMode: return mode @property - def target_humidity(self) -> int: - """Return target humidity.""" - return self.data["target_humidity"] - - @property + @sensor( + "Water Level", + unit="%", + device_class="measurement", + icon="mdi:water-check", + entity_category="diagnostic", + ) def water_level(self) -> Optional[int]: """Return current water level in percent. @@ -144,6 +335,12 @@ def water_level(self) -> Optional[int]: return int(min(water_level / 1.2, 100)) @property + @sensor( + "Water Tank Attached", + device_class="connectivity", + icon="mdi:car-coolant-level", + entity_category="diagnostic", + ) def water_tank_detached(self) -> bool: """True if the water tank is detached. @@ -152,13 +349,13 @@ def water_tank_detached(self) -> bool: return self.data["water_level"] == 127 @property - def dry(self) -> Optional[bool]: - """Return True if dry mode is on.""" - if self.data["dry"] is not None: - return self.data["dry"] - return None - - @property + @sensor( + "Use Time", + unit="s", + device_class="total_increasing", + icon="mdi:progress-clock", + entity_category="diagnostic", + ) def use_time(self) -> int: """Return how long the device has been active in seconds.""" return self.data["use_time"] @@ -176,6 +373,13 @@ def button_pressed(self) -> PressedButton: return button @property + @sensor( + "Target Motor Speed", + unit="rpm", + device_class="measurement", + icon="mdi:fast-forward", + entity_category="diagnostic", + ) def motor_speed(self) -> int: """Return target speed of the motor.""" return self.data["speed_level"] @@ -183,77 +387,32 @@ def motor_speed(self) -> int: # Environment @property - def humidity(self) -> int: - """Return current humidity.""" - return self.data["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 - - @property + @sensor("Temperature", unit="°F", device_class="temperature") def fahrenheit(self) -> Optional[float]: """Return current temperature in fahrenheit, if available.""" if self.data["fahrenheit"] is not None: return round(self.data["fahrenheit"], 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_brightness(self) -> Optional[LedBrightness]: - """Return brightness of the LED.""" - - if self.data["led_brightness"] is not None: - try: - return LedBrightness(self.data["led_brightness"]) - except ValueError as e: - _LOGGER.exception("Cannot parse led_brightness: %s", e) - return None - - return None - - # Physical Control Locked - - @property - def child_lock(self) -> bool: - """Return True if child lock is on.""" - return self.data["child_lock"] - # Other @property - def actual_speed(self) -> int: - """Return real speed of the motor.""" - return self.data["actual_speed"] - - @property + @sensor( + "Power On Time", + unit="s", + device_class="total_increasing", + icon="mdi:progress-clock", + entity_category="diagnostic", + ) def power_time(self) -> int: """Return how long the device has been powered in seconds.""" return self.data["power_time"] - @property - def clean_mode(self) -> bool: - """Return True if clean mode is active.""" - return self.data["clean_mode"] - class AirHumidifierMiot(MiotDevice): """Main class representing the air humidifier which uses MIoT protocol.""" - _mappings = _MAPPINGS + _mappings = _MAPPINGS_CA4 @command( default_output=format_output( @@ -381,3 +540,238 @@ def set_dry(self, dry: bool): def set_clean_mode(self, clean_mode: bool): """Set clean mode on/off.""" return self.set_property("clean_mode", clean_mode) + + +class AirHumidifierMiotCA6Status(AirHumidifierMiotCommonStatus): + """Container for status reports from the air humidifier. + + Xiaomi Smartmi Evaporation Air Humidifier 3 (zhimi.humidifier.ca6) 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': 0}, + {'did': 'target_humidity', 'siid': 2, 'piid': 6, 'code': 0, 'value': 50}, + {'did': 'water_level', 'siid': 2, 'piid': 7, 'code': 0, 'value': 1}, + {'did': 'dry', 'siid': 2, 'piid': 8, 'code': 0, 'value': True}, + {'did': 'status', 'siid': 2, 'piid': 9, 'code': 0, 'value': 2}, + {'did': 'temperature', 'siid': 3, 'piid': 7, 'code': 0, 'value': 19.0}, + {'did': 'humidity', 'siid': 3, 'piid': 9, 'code': 0, 'value': 51}, + {'did': 'buzzer', 'siid': 4, 'piid': 1, 'code': 0, 'value': False}, + {'did': 'led_brightness', 'siid': 5, 'piid': 2, 'code': 0, 'value': 2}, + {'did': 'child_lock', 'siid': 6, 'piid': 1, 'code': 0, 'value': False}, + {'did': 'actual_speed', 'siid': 7, 'piid': 1, 'code': 0, 'value': 1100}, + {'did': 'clean_mode', 'siid': 7, 'piid': 5, 'code': 0, 'value': False} + {'did': 'self_clean_percent, 'siid': 7, 'piid': 6, 'code': 0, 'value': 0}, + {'did': 'pump_state, 'siid': 7, 'piid': 7, 'code': 0, 'value': False}, + {'did': 'pump_cnt', 'siid': 7, 'piid': 8, 'code': 0, 'value': 1000}, + ] + """ + + def __init__(self, data: Dict[str, Any]) -> None: + self.data = data + super().__init__(self.data) + self.embed("common", AirHumidifierMiotCommonStatus(self.data)) + + # Air Humidifier 3 + + @property + @setting( + name="Operation Mode", + setter_name="set_mode", + ) + def mode(self) -> OperationModeCA6: + """Return current operation mode.""" + + try: + mode = OperationModeCA6(self.data["mode"]) + except ValueError as e: + _LOGGER.exception("Cannot parse mode: %s", e) + return OperationModeCA6.Auto + + return mode + + @property + @sensor( + "Water Level", + unit="%", + device_class="measurement", + icon="mdi:water-check", + entity_category="diagnostic", + ) + def water_level(self) -> Optional[int]: + """Return current water level (empty/min, normal, full/max). + + 0 - empty/min, 1 - normal, 2 - full/max + """ + water_level = self.data["water_level"] + return {0: 0, 1: 50, 2: 100}.get(water_level) + + @property + @sensor( + "Operation status", + device_class="measurement", + entity_category="diagnostic", + ) + def status(self) -> OperationStatusCA6: + """Return current status.""" + + try: + status = OperationStatusCA6(self.data["status"]) + except ValueError as e: + _LOGGER.exception("Cannot parse status: %s", e) + return OperationStatusCA6.Close + + return status + + # Other + + @property + @sensor( + "Self-clean Percent", + unit="s", + device_class="total_increasing", + icon="mdi:progress-clock", + entity_category="diagnostic", + ) + def self_clean_percent(self) -> int: + """Return time in minutes (from 0 to 30) of self-cleaning procedure.""" + return self.data["self_clean_percent"] + + @property + @sensor( + "Pump State", + entity_category="diagnostic", + ) + def pump_state(self) -> bool: + """Return pump state.""" + return self.data["pump_state"] + + @property + @sensor( + "Pump Cnt", + entity_category="diagnostic", + ) + def pump_cnt(self) -> int: + """Return pump-cnt.""" + return self.data["pump_cnt"] + + +class AirHumidifierMiotCA6(MiotDevice): + """Main class representing zhimi.humidifier.ca6 air humidifier which uses MIoT protocol.""" + + _mappings = _MAPPINGS_CA6 + + @command( + default_output=format_output( + "", + "Power: {result.power}\n" + "Error: {result.error}\n" + "Target Humidity: {result.target_humidity} %\n" + "Humidity: {result.humidity} %\n" + "Temperature: {result.temperature} °C\n" + "Water Level: {result.water_level} %\n" + "Mode: {result.mode}\n" + "Status: {result.status}\n" + "LED brightness: {result.led_brightness}\n" + "Buzzer: {result.buzzer}\n" + "Child lock: {result.child_lock}\n" + "Dry mode: {result.dry}\n" + "Actual motor speed: {result.actual_speed} rpm\n" + "Clean mode: {result.clean_mode}\n" + "Self clean percent: {result.self_clean_percent} minutes\n" + "Pump state: {result.pump_state}\n" + "Pump cnt: {result.pump_cnt}\n", + ) + ) + def status(self) -> AirHumidifierMiotCA6Status: + """Retrieve properties.""" + + return AirHumidifierMiotCA6Status( + { + 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 < 30 or humidity > 60: + raise ValueError( + "Invalid target humidity: %s. Must be between 30 and 60" % humidity + ) + # HA sends humidity in float, e.g. 45.0 + # ca6 does accept only int values, e.g. 45 + return self.set_property("target_humidity", int(humidity)) + + @command( + click.argument("mode", type=EnumType(OperationModeCA6)), + 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("brightness", type=EnumType(LedBrightness)), + default_output=format_output("Setting LED brightness to {brightness}"), + ) + def set_led_brightness(self, brightness: LedBrightness): + """Set led brightness.""" + return self.set_property("led_brightness", brightness.value) + + @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("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.set_property("child_lock", lock) + + @command( + click.argument("dry", type=bool), + default_output=format_output( + lambda dry: "Turning on dry mode" if dry else "Turning off dry mode" + ), + ) + def set_dry(self, dry: bool): + """Set dry mode on/off.""" + return self.set_property("dry", dry) + + @command( + click.argument("clean_mode", type=bool), + default_output=format_output( + lambda clean_mode: ( + "Turning on clean mode" if clean_mode else "Turning off clean mode" + ) + ), + ) + def set_clean_mode(self, clean_mode: bool): + """Set clean mode on/off.""" + return self.set_property("clean_mode", clean_mode) diff --git a/miio/integrations/zhimi/humidifier/tests/test_airhumidifier_miot_ca6.py b/miio/integrations/zhimi/humidifier/tests/test_airhumidifier_miot_ca6.py new file mode 100644 index 000000000..d89bb7f1b --- /dev/null +++ b/miio/integrations/zhimi/humidifier/tests/test_airhumidifier_miot_ca6.py @@ -0,0 +1,199 @@ +import pytest + +from miio.tests.dummies import DummyMiotDevice + +from .. import AirHumidifierMiotCA6 +from ..airhumidifier_miot import LedBrightness, OperationModeCA6, OperationStatusCA6 + +_INITIAL_STATE = { + "power": True, + "fault": 0, + "mode": 0, + "target_humidity": 40, + "water_level": 1, + "dry": True, + "status": 2, + "temperature": 19, + "humidity": 51, + "buzzer": False, + "led_brightness": 2, + "child_lock": False, + "actual_speed": 1100, + "clean_mode": False, + "self_clean_percent": 0, + "pump_state": False, + "pump_cnt": 1000, +} + + +class DummyAirHumidifierMiotCA6(DummyMiotDevice, AirHumidifierMiotCA6): + 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_speed": lambda x: self._set_state("speed_level", x), + "set_target_humidity": lambda x: self._set_state("target_humidity", x), + "set_mode": lambda x: self._set_state("mode", x), + "set_led_brightness": lambda x: self._set_state("led_brightness", x), + "set_buzzer": lambda x: self._set_state("buzzer", x), + "set_child_lock": lambda x: self._set_state("child_lock", x), + "set_dry": lambda x: self._set_state("dry", x), + "set_clean_mode": lambda x: self._set_state("clean_mode", x), + } + super().__init__(*args, **kwargs) + + +@pytest.fixture() +def dev(request): + yield DummyAirHumidifierMiotCA6() + + +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 == OperationModeCA6(_INITIAL_STATE["mode"]) + assert status.target_humidity == _INITIAL_STATE["target_humidity"] + assert status.water_level == {0: 0, 1: 50, 2: 100}.get( + int(_INITIAL_STATE["water_level"]) + ) + assert status.dry == _INITIAL_STATE["dry"] + assert status.temperature == _INITIAL_STATE["temperature"] + assert status.humidity == _INITIAL_STATE["humidity"] + assert status.buzzer == _INITIAL_STATE["buzzer"] + assert status.led_brightness == LedBrightness(_INITIAL_STATE["led_brightness"]) + assert status.child_lock == _INITIAL_STATE["child_lock"] + assert status.actual_speed == _INITIAL_STATE["actual_speed"] + assert status.actual_speed == _INITIAL_STATE["actual_speed"] + assert status.clean_mode == _INITIAL_STATE["clean_mode"] + assert status.self_clean_percent == _INITIAL_STATE["self_clean_percent"] + assert status.pump_state == _INITIAL_STATE["pump_state"] + assert status.pump_cnt == _INITIAL_STATE["pump_cnt"] + + +def test_set_target_humidity(dev): + def target_humidity(): + return dev.status().target_humidity + + dev.set_target_humidity(30) + assert target_humidity() == 30 + dev.set_target_humidity(60) + assert target_humidity() == 60 + + with pytest.raises(ValueError): + dev.set_target_humidity(29) + + with pytest.raises(ValueError): + dev.set_target_humidity(61) + + +def test_set_mode(dev): + def mode(): + return dev.status().mode + + dev.set_mode(OperationModeCA6.Auto) + assert mode() == OperationModeCA6.Auto + + dev.set_mode(OperationModeCA6.Fav) + assert mode() == OperationModeCA6.Fav + + dev.set_mode(OperationModeCA6.Sleep) + assert mode() == OperationModeCA6.Sleep + + +def test_set_led_brightness(dev): + def led_brightness(): + return dev.status().led_brightness + + dev.set_led_brightness(LedBrightness.Bright) + assert led_brightness() == LedBrightness.Bright + + dev.set_led_brightness(LedBrightness.Dim) + assert led_brightness() == LedBrightness.Dim + + dev.set_led_brightness(LedBrightness.Off) + assert led_brightness() == LedBrightness.Off + + +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_child_lock(dev): + def child_lock(): + return dev.status().child_lock + + dev.set_child_lock(True) + assert child_lock() is True + + dev.set_child_lock(False) + assert child_lock() is False + + +def test_set_dry(dev): + def dry(): + return dev.status().dry + + dev.set_dry(True) + assert dry() is True + + dev.set_dry(False) + assert dry() is False + + +def test_set_clean_mode(dev): + def clean_mode(): + return dev.status().clean_mode + + dev.set_clean_mode(True) + assert clean_mode() is True + + dev.set_clean_mode(False) + assert clean_mode() is False + + +@pytest.mark.parametrize("given,expected", [(0, 0), (1, 50), (2, 100)]) +def test_water_level(dev, given, expected): + dev.set_property("water_level", given) + assert dev.status().water_level == expected + + +def test_op_status(dev): + def op_status(): + return dev.status().status + + dev.set_property("status", OperationStatusCA6.Close) + assert op_status() == OperationStatusCA6.Close + + dev.set_property("status", OperationStatusCA6.Work) + assert op_status() == OperationStatusCA6.Work + + dev.set_property("status", OperationStatusCA6.Dry) + assert op_status() == OperationStatusCA6.Dry + + dev.set_property("status", OperationStatusCA6.Clean) + assert op_status() == OperationStatusCA6.Clean