diff --git a/miio/airhumidifier.py b/miio/airhumidifier.py index 54b56f370..4182c78ad 100644 --- a/miio/airhumidifier.py +++ b/miio/airhumidifier.py @@ -12,11 +12,11 @@ MODEL_HUMIDIFIER_V1 = 'zhimi.humidifier.v1' MODEL_HUMIDIFIER_CA1 = 'zhimi.humidifier.ca1' +MODEL_HUMIDIFIER_CB1 = 'zhimi.humidifier.cb1' AVAILABLE_PROPERTIES_COMMON = [ 'power', 'mode', - 'temp_dec', 'humidity', 'buzzer', 'led_b', @@ -27,8 +27,9 @@ ] AVAILABLE_PROPERTIES = { - MODEL_HUMIDIFIER_V1: AVAILABLE_PROPERTIES_COMMON + ['trans_level', 'button_pressed'], - MODEL_HUMIDIFIER_CA1: AVAILABLE_PROPERTIES_COMMON + ['speed', 'depth', 'dry'], + MODEL_HUMIDIFIER_V1: AVAILABLE_PROPERTIES_COMMON + ['temp_dec', 'trans_level', 'button_pressed'], + MODEL_HUMIDIFIER_CA1: AVAILABLE_PROPERTIES_COMMON + ['temp_dec', 'speed', 'depth', 'dry'], + MODEL_HUMIDIFIER_CB1: AVAILABLE_PROPERTIES_COMMON + ['temperature', 'speed', 'depth', 'dry'] } @@ -85,8 +86,10 @@ def mode(self) -> OperationMode: @property def temperature(self) -> Optional[float]: """Current temperature, if available.""" - if self.data["temp_dec"] is not None: + if "temp_dec" in self.data and self.data["temp_dec"] is not None: return self.data["temp_dec"] / 10.0 + if "temperature" in self.data and self.data["temperature"] is not None: + return self.data["temperature"] return None @property @@ -285,8 +288,8 @@ def status(self) -> AirHumidifierStatus: # properties are divided into multiple requests _props_per_request = 15 - # The CA1 is limited to a single property per request - if self.model == MODEL_HUMIDIFIER_CA1: + # The CA1 and CB1 are limited to a single property per request + if self.model in [MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1]: _props_per_request = 1 _props = properties.copy() @@ -335,7 +338,7 @@ def set_mode(self, mode: OperationMode): ) def set_led_brightness(self, brightness: LedBrightness): """Set led brightness.""" - if self.model == MODEL_HUMIDIFIER_CA1: + if self.model in [MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1]: return self.send("set_led_b", [str(brightness.value)]) return self.send("set_led_b", [brightness.value]) @@ -414,3 +417,10 @@ def __init__(self, ip: str = None, token: str = None, start_id: int = 0, debug: int = 0, lazy_discover: bool = True) -> None: super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_HUMIDIFIER_CA1) + + +class AirHumidifierCB1(AirHumidifier): + def __init__(self, ip: str = None, token: str = None, start_id: int = 0, + debug: int = 0, lazy_discover: bool = True) -> None: + super().__init__(ip, token, start_id, debug, lazy_discover, + model=MODEL_HUMIDIFIER_CB1) diff --git a/miio/discovery.py b/miio/discovery.py index f0ea31720..22dca3c38 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -14,7 +14,7 @@ from .airconditioningcompanion import (MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3, ) from .airqualitymonitor import (MODEL_AIRQUALITYMONITOR_V1, MODEL_AIRQUALITYMONITOR_B1, ) -from .airhumidifier import (MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_V1, ) +from .airhumidifier import (MODEL_HUMIDIFIER_CB1, MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_V1, ) from .chuangmi_plug import (MODEL_CHUANGMI_PLUG_V1, MODEL_CHUANGMI_PLUG_V2, MODEL_CHUANGMI_PLUG_V3, MODEL_CHUANGMI_PLUG_M1, MODEL_CHUANGMI_PLUG_M3, MODEL_CHUANGMI_PLUG_HMI205, ) @@ -53,6 +53,7 @@ "chuangmi-ir-v2": ChuangmiIr, "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), "yunmi-waterpuri-v2": WaterPurifier, "philips-light-bulb": PhilipsBulb, # cannot be discovered via mdns "philips-light-candle": PhilipsBulb, # cannot be discovered via mdns diff --git a/miio/tests/test_airhumidifier.py b/miio/tests/test_airhumidifier.py index 3f3a51267..d9baa265d 100644 --- a/miio/tests/test_airhumidifier.py +++ b/miio/tests/test_airhumidifier.py @@ -5,7 +5,7 @@ from miio import AirHumidifier from miio.airhumidifier import (OperationMode, LedBrightness, AirHumidifierStatus, AirHumidifierException, - MODEL_HUMIDIFIER_V1, MODEL_HUMIDIFIER_CA1) + MODEL_HUMIDIFIER_V1, MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_CB1) from .dummies import DummyDevice from miio.device import DeviceInfo @@ -427,3 +427,218 @@ def dry(): self.device.set_dry(False) assert dry() is False + + +class DummyAirHumidifierCB1(DummyDevice, AirHumidifier): + def __init__(self, *args, **kwargs): + self.model = MODEL_HUMIDIFIER_CB1 + self.dummy_device_info = { + 'fw_ver': '1.2.9_5033', + 'token': '68ffffffffffffffffffffffffffffff', + 'otu_stat': [101, 74, 5343, 0, 5327, 407], + 'mmfree': 228248, + 'netif': {'gw': '192.168.0.1', + 'localIp': '192.168.0.25', + 'mask': '255.255.255.0'}, + 'ott_stat': [0, 0, 0, 0], + 'model': 'zhimi.humidifier.v1', + 'cfg_time': 0, + 'life': 575661, + 'ap': {'rssi': -35, 'ssid': 'ap', 'bssid': 'FF:FF:FF:FF:FF:FF'}, + 'wifi_fw_ver': 'SD878x-14.76.36.p84-702.1.0-WM', + 'hw_ver': 'MW300', + 'ot': 'otu', + 'mac': '78:11:FF:FF:FF:FF' + } + self.device_info = None + + self.state = { + 'power': 'on', + 'mode': 'medium', + 'humidity': 33, + 'buzzer': 'off', + 'led_b': 2, + 'child_lock': 'on', + 'limit_hum': 40, + 'use_time': 941100, + 'hw_version': 0, + # Additional attributes of the zhimi.humidifier.cb1 + 'temperature': 29.4, + 'speed': 100, + 'depth': 1, + 'dry': 'off', + } + self.return_values = { + 'get_prop': self._get_state, + 'set_power': lambda x: self._set_state("power", x), + 'set_mode': lambda x: self._set_state("mode", x), + 'set_led_b': lambda x: self._set_state("led_b", [int(x[0])]), + 'set_buzzer': lambda x: self._set_state("buzzer", x), + 'set_child_lock': lambda x: self._set_state("child_lock", x), + 'set_limit_hum': lambda x: self._set_state("limit_hum", x), + 'set_dry': lambda x: self._set_state("dry", 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 + + +@pytest.fixture(scope="class") +def airhumidifiercb1(request): + request.cls.device = DummyAirHumidifierCB1() + # TODO add ability to test on a real device + + +@pytest.mark.usefixtures("airhumidifiercb1") +class TestAirHumidifierCB1(TestCase): + def is_on(self): + return self.device.status().is_on + + def state(self): + return self.device.status() + + 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(self): + self.device._reset_state() + + device_info = DeviceInfo(self.device.dummy_device_info) + + assert repr(self.state()) == repr(AirHumidifierStatus(self.device.start_state, device_info)) + + assert self.is_on() is True + 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().led_brightness == LedBrightness(self.device.start_state["led_b"]) + assert self.state().buzzer == (self.device.start_state["buzzer"] == 'on') + assert self.state().child_lock == (self.device.start_state["child_lock"] == 'on') + assert self.state().target_humidity == self.device.start_state["limit_hum"] + assert self.state().trans_level is None + assert self.state().motor_speed == self.device.start_state["speed"] + assert self.state().depth == self.device.start_state["depth"] + assert self.state().dry == (self.device.start_state["dry"] == 'on') + assert self.state().use_time == self.device.start_state["use_time"] + assert self.state().hardware_version == self.device.start_state["hw_version"] + assert self.state().button_pressed is None + + assert self.state().firmware_version == device_info.firmware_version + assert self.state().firmware_version_major == device_info.firmware_version.rsplit('_', 1)[0] + assert self.state().firmware_version_minor == int(device_info.firmware_version.rsplit('_', 1)[1]) + assert self.state().strong_mode_enabled is False + + def test_set_mode(self): + def mode(): + return self.device.status().mode + + self.device.set_mode(OperationMode.Silent) + assert mode() == OperationMode.Silent + + self.device.set_mode(OperationMode.Medium) + assert mode() == OperationMode.Medium + + self.device.set_mode(OperationMode.High) + assert mode() == OperationMode.High + + def test_set_led_brightness(self): + def led_brightness(): + return self.device.status().led_brightness + + self.device.set_led_brightness(LedBrightness.Bright) + assert led_brightness() == LedBrightness.Bright + + self.device.set_led_brightness(LedBrightness.Dim) + assert led_brightness() == LedBrightness.Dim + + self.device.set_led_brightness(LedBrightness.Off) + assert led_brightness() == LedBrightness.Off + + def test_set_led(self): + def led_brightness(): + return self.device.status().led_brightness + + self.device.set_led(True) + assert led_brightness() == LedBrightness.Bright + + self.device.set_led(False) + 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 + + 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_b"] = None + + assert self.state().led_brightness is None + + def test_set_target_humidity(self): + def target_humidity(): + return self.device.status().target_humidity + + self.device.set_target_humidity(30) + assert target_humidity() == 30 + self.device.set_target_humidity(60) + assert target_humidity() == 60 + self.device.set_target_humidity(80) + assert target_humidity() == 80 + + with pytest.raises(AirHumidifierException): + self.device.set_target_humidity(-1) + + with pytest.raises(AirHumidifierException): + self.device.set_target_humidity(20) + + with pytest.raises(AirHumidifierException): + self.device.set_target_humidity(90) + + with pytest.raises(AirHumidifierException): + self.device.set_target_humidity(110) + + def test_set_child_lock(self): + def child_lock(): + return self.device.status().child_lock + + self.device.set_child_lock(True) + assert child_lock() is True + + self.device.set_child_lock(False) + assert child_lock() is False + + def test_set_dry(self): + def dry(): + return self.device.status().dry + + self.device.set_dry(True) + assert dry() is True + + self.device.set_dry(False) + assert dry() is False