From 82bbf8d3fa20060030736e6afe133034c055901f Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 00:08:38 +0800 Subject: [PATCH 01/10] add xiaomi.airp.va2b support --- .../zhimi/airpurifier/airpurifier_miot.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py index e36b28270..6b9c7b06d 100644 --- a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py @@ -105,6 +105,36 @@ "led_brightness": {"siid": 13, "piid": 2}, } +# https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-purifier:0000A007:xiaomi-va2b:1 +_MAPPING_VA2B = { + # Air Purifier + "power": {"siid": 2, "piid": 1}, + "mode": {"siid": 2, "piid": 4}, + "fan_level": {"siid": 2, "piid": 5}, + "anion": {"siid": 2, "piid": 6}, + # Environment + "humidity": {"siid": 3, "piid": 1}, + "temperature": {"siid": 3, "piid": 2}, + "aqi": {"siid": 3, "piid": 4}, + # Filter + "filter_life_remaining": {"siid": 4, "piid": 1}, + "filter_hours_used": {"siid": 4, "piid": 2}, + "filter_left_time": {"siid": 4, "piid": 3}, + # Alarm + "buzzer": {"siid": 6, "piid": 1}, + # Physical Control Locked + "child_lock": {"siid": 8, "piid": 1}, + # custom-service + "motor_speed": {"siid": 9, "piid": 1}, + # RFID + "filter_rfid_tag": {"siid": 12, "piid": 1}, + "filter_rfid_product_id": {"siid": 12, "piid": 3}, + # Screen + "led_brightness": {"siid": 13, "piid": 1}, + # Air Purifier Favorite + "favorite_level": {"siid": 14, "piid": 1}, +} + # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-vb4:1 _MAPPING_VB4 = { # Air Purifier @@ -276,6 +306,7 @@ "zhimi.airp.mb5": _MAPPING_VA2, # airpurifier 4 "zhimi.airp.mb5a": _MAPPING_VA2, # airpurifier 4 "zhimi.airp.va2": _MAPPING_VA2, # airpurifier 4 pro + "xiaomi.airp.va2b": _MAPPING_VA2B, # airpurifier 4 pro "zhimi.airp.vb4": _MAPPING_VB4, # airpurifier 4 pro "zhimi.airpurifier.rma1": _MAPPING_RMA1, # airpurifier 4 lite "zhimi.airpurifier.rma2": _MAPPING_RMA2, # airpurifier 4 lite @@ -286,6 +317,7 @@ # Models requiring reversed led brightness value REVERSED_LED_BRIGHTNESS = [ "zhimi.airp.va2", + "xiaomi.airp.va2b", "zhimi.airp.mb5", "zhimi.airp.mb5a", "zhimi.airp.vb4", From 74ee5e2a65c3813b984f3059a206468272970dd6 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 00:21:21 +0800 Subject: [PATCH 02/10] fix lint --- miio/integrations/zhimi/airpurifier/airpurifier_miot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py index 6b9c7b06d..4a6364561 100644 --- a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py @@ -306,7 +306,7 @@ "zhimi.airp.mb5": _MAPPING_VA2, # airpurifier 4 "zhimi.airp.mb5a": _MAPPING_VA2, # airpurifier 4 "zhimi.airp.va2": _MAPPING_VA2, # airpurifier 4 pro - "xiaomi.airp.va2b": _MAPPING_VA2B, # airpurifier 4 pro + "xiaomi.airp.va2b": _MAPPING_VA2B, # airpurifier 4 pro "zhimi.airp.vb4": _MAPPING_VB4, # airpurifier 4 pro "zhimi.airpurifier.rma1": _MAPPING_RMA1, # airpurifier 4 lite "zhimi.airpurifier.rma2": _MAPPING_RMA2, # airpurifier 4 lite From fcdfa878342d0405f83b67c5fc6c285f57fc3801 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 00:34:32 +0800 Subject: [PATCH 03/10] add tests --- .../tests/test_airpurifier_miot.py | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index b4b4411c3..d6e52b47e 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -69,6 +69,26 @@ "button_pressed": "power", } +_INITIAL_STATE_VA2B = { + "power": True, + "aqi": 10, + "anion": True, + "humidity": 62, + "temperature": 18.599999, + "fan_level": 2, + "mode": 0, + "led_brightness": 1, + "buzzer": False, + "favorite_level": 10, + "filter_life_remaining": 80, + "filter_hours_used": 682, + "filter_left_time": 309, + "motor_speed": 354, + "filter_rfid_product_id": "0:0:41:30", + "filter_rfid_tag": "10:20:30:40:50:60:7", + "button_pressed": "power", +} + class DummyAirPurifierMiot(DummyMiotDevice, AirPurifierMiot): def __init__(self, *args, **kwargs): @@ -305,6 +325,11 @@ def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE_VA2 super().__init__(*args, **kwargs) +class DummyAirPurifierMiotVA2B(DummyAirPurifierMiot): + def __init__(self, *args, **kwargs): + self._model = "xiaomi.airp.va2b" + self.state = _INITIAL_STATE_VA2B + super().__init__(*args, **kwargs) class DummyAirPurifierMiotMB5(DummyAirPurifierMiot): def __init__(self, *args, **kwargs): @@ -317,7 +342,6 @@ def __init__(self, *args, **kwargs): def airpurifierVA2(request): request.cls.device = DummyAirPurifierMiotVA2() - @pytest.mark.usefixtures("airpurifierVA2") class TestAirPurifierVA2(TestCase): def test_status(self): @@ -373,3 +397,61 @@ def anion(): self.device.set_anion(False) assert anion() is False + +@pytest.fixture(scope="function") +def airpurifierVA2B(request): + request.cls.device = DummyAirPurifierMiotVA2B() + +@pytest.mark.usefixtures("airpurifierVA2B") +class TestAirPurifierVA2(TestCase): + def test_status(self): + status = self.device.status() + assert status.is_on is _INITIAL_STATE_VA2["power"] + assert status.anion == _INITIAL_STATE_VA2["anion"] + assert status.aqi == _INITIAL_STATE_VA2["aqi"] + assert status.humidity == _INITIAL_STATE_VA2["humidity"] + assert status.temperature == 18.6 + assert status.fan_level == _INITIAL_STATE_VA2["fan_level"] + assert status.mode == OperationMode(_INITIAL_STATE_VA2["mode"]) + assert status.led is None + assert status.led_brightness == LedBrightness( + _INITIAL_STATE_VA2["led_brightness"] + ) + assert status.buzzer == _INITIAL_STATE_VA2["buzzer"] + assert status.child_lock == _INITIAL_STATE_VA2["child_lock"] + assert status.favorite_level == _INITIAL_STATE_VA2["favorite_level"] + assert ( + status.filter_life_remaining == _INITIAL_STATE_VA2["filter_life_remaining"] + ) + assert status.filter_hours_used == _INITIAL_STATE_VA2["filter_hours_used"] + assert status.filter_left_time == _INITIAL_STATE_VA2["filter_left_time"] + assert status.use_time is None + assert status.motor_speed == _INITIAL_STATE_VA2["motor_speed"] + assert ( + status.filter_rfid_product_id + == _INITIAL_STATE_VA2["filter_rfid_product_id"] + ) + assert status.filter_type == FilterType.AntiBacterial + + 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_anion(self): + def anion(): + return self.device.status().anion + + self.device.set_anion(True) + assert anion() is True + + self.device.set_anion(False) + assert anion() is False From 381a1e1cc52013b000e684e33f2c3fdf777344c6 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 00:34:52 +0800 Subject: [PATCH 04/10] lint --- .../zhimi/airpurifier/tests/test_airpurifier_miot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index d6e52b47e..89400744b 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -325,12 +325,14 @@ def __init__(self, *args, **kwargs): self.state = _INITIAL_STATE_VA2 super().__init__(*args, **kwargs) + class DummyAirPurifierMiotVA2B(DummyAirPurifierMiot): def __init__(self, *args, **kwargs): self._model = "xiaomi.airp.va2b" self.state = _INITIAL_STATE_VA2B super().__init__(*args, **kwargs) + class DummyAirPurifierMiotMB5(DummyAirPurifierMiot): def __init__(self, *args, **kwargs): self._model = "zhimi.airp.mb5" @@ -342,6 +344,7 @@ def __init__(self, *args, **kwargs): def airpurifierVA2(request): request.cls.device = DummyAirPurifierMiotVA2() + @pytest.mark.usefixtures("airpurifierVA2") class TestAirPurifierVA2(TestCase): def test_status(self): @@ -398,10 +401,12 @@ def anion(): self.device.set_anion(False) assert anion() is False + @pytest.fixture(scope="function") def airpurifierVA2B(request): request.cls.device = DummyAirPurifierMiotVA2B() + @pytest.mark.usefixtures("airpurifierVA2B") class TestAirPurifierVA2(TestCase): def test_status(self): From 9b5efaa4c5006b7f95fa278f9543e60185568449 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 00:44:32 +0800 Subject: [PATCH 05/10] ag --- .../zhimi/airpurifier/tests/test_airpurifier_miot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index 89400744b..ef9a2b360 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -408,7 +408,7 @@ def airpurifierVA2B(request): @pytest.mark.usefixtures("airpurifierVA2B") -class TestAirPurifierVA2(TestCase): +class TestAirPurifierVA2B(TestCase): def test_status(self): status = self.device.status() assert status.is_on is _INITIAL_STATE_VA2["power"] From c5f04a08f164b787518a55bdcf7d47b7eed9d053 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 01:08:23 +0800 Subject: [PATCH 06/10] fixes --- miio/integrations/zhimi/airpurifier/airpurifier_miot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py index 4a6364561..380907638 100644 --- a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py @@ -317,7 +317,6 @@ # Models requiring reversed led brightness value REVERSED_LED_BRIGHTNESS = [ "zhimi.airp.va2", - "xiaomi.airp.va2b", "zhimi.airp.mb5", "zhimi.airp.mb5a", "zhimi.airp.vb4", From 4d2310eebd3338e1ca635adc65d666adee11099e Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 01:46:46 +0800 Subject: [PATCH 07/10] fixes --- .../zhimi/airpurifier/airpurifier_miot.py | 4 ++- .../tests/test_airpurifier_miot.py | 30 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py index 380907638..c1a826126 100644 --- a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py @@ -599,8 +599,10 @@ def status(self) -> AirPurifierMiotStatus: return AirPurifierMiotStatus( { + # max_properties limited to 10 to avoid "user ack timeout" + # messages from some of the devices. (e.g. xiaomi.airp.va2b) prop["did"]: prop["value"] if prop["code"] == 0 else None - for prop in self.get_properties_for_mapping() + for prop in self.get_properties_for_mapping(max_properties=10) }, self.model, ) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index ef9a2b360..24588dab3 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -411,30 +411,30 @@ def airpurifierVA2B(request): class TestAirPurifierVA2B(TestCase): def test_status(self): status = self.device.status() - assert status.is_on is _INITIAL_STATE_VA2["power"] - assert status.anion == _INITIAL_STATE_VA2["anion"] - assert status.aqi == _INITIAL_STATE_VA2["aqi"] - assert status.humidity == _INITIAL_STATE_VA2["humidity"] + assert status.is_on is _INITIAL_STATE_VA2B["power"] + assert status.anion == _INITIAL_STATE_VA2B["anion"] + assert status.aqi == _INITIAL_STATE_VA2B["aqi"] + assert status.humidity == _INITIAL_STATE_VA2B["humidity"] assert status.temperature == 18.6 - assert status.fan_level == _INITIAL_STATE_VA2["fan_level"] - assert status.mode == OperationMode(_INITIAL_STATE_VA2["mode"]) + assert status.fan_level == _INITIAL_STATE_VA2B["fan_level"] + assert status.mode == OperationMode(_INITIAL_STATE_VA2B["mode"]) assert status.led is None assert status.led_brightness == LedBrightness( - _INITIAL_STATE_VA2["led_brightness"] + _INITIAL_STATE_VA2B["led_brightness"] ) - assert status.buzzer == _INITIAL_STATE_VA2["buzzer"] - assert status.child_lock == _INITIAL_STATE_VA2["child_lock"] - assert status.favorite_level == _INITIAL_STATE_VA2["favorite_level"] + assert status.buzzer == _INITIAL_STATE_VA2B["buzzer"] + assert status.child_lock == _INITIAL_STATE_VA2B["child_lock"] + assert status.favorite_level == _INITIAL_STATE_VA2B["favorite_level"] assert ( - status.filter_life_remaining == _INITIAL_STATE_VA2["filter_life_remaining"] + status.filter_life_remaining == _INITIAL_STATE_VA2B["filter_life_remaining"] ) - assert status.filter_hours_used == _INITIAL_STATE_VA2["filter_hours_used"] - assert status.filter_left_time == _INITIAL_STATE_VA2["filter_left_time"] + assert status.filter_hours_used == _INITIAL_STATE_VA2B["filter_hours_used"] + assert status.filter_left_time == _INITIAL_STATE_VA2B["filter_left_time"] assert status.use_time is None - assert status.motor_speed == _INITIAL_STATE_VA2["motor_speed"] + assert status.motor_speed == _INITIAL_STATE_VA2B["motor_speed"] assert ( status.filter_rfid_product_id - == _INITIAL_STATE_VA2["filter_rfid_product_id"] + == _INITIAL_STATE_VA2B["filter_rfid_product_id"] ) assert status.filter_type == FilterType.AntiBacterial From d0cf68be3dc5e3a1963cd13fcda9f564c867de65 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 01:48:09 +0800 Subject: [PATCH 08/10] no limit at all! --- miio/integrations/roidmi/vacuum/roidmivacuum_miot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py index fcea53b4d..50cf9425f 100644 --- a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py +++ b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py @@ -558,7 +558,6 @@ def status(self) -> RoidmiVacuumStatus: return RoidmiVacuumStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None - # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } ) @@ -569,7 +568,6 @@ def consumable_status(self) -> RoidmiConsumableStatus: return RoidmiConsumableStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None - # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } ) @@ -580,7 +578,6 @@ def cleaning_summary(self) -> RoidmiCleaningSummary: return RoidmiCleaningSummary( { prop["did"]: prop["value"] if prop["code"] == 0 else None - # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } ) From 4d23d255c61814a8bdb31a0eceba469dbc3412c1 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 17 Jun 2024 01:52:46 +0800 Subject: [PATCH 09/10] fixes --- .../zhimi/airpurifier/tests/test_airpurifier_miot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index 24588dab3..6f94ba99d 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -79,6 +79,7 @@ "mode": 0, "led_brightness": 1, "buzzer": False, + "child_lock": False, "favorite_level": 10, "filter_life_remaining": 80, "filter_hours_used": 682, From 6cf9755ae95bfa89645cd807b6841cf3695959ad Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Wed, 26 Jun 2024 14:38:27 +0800 Subject: [PATCH 10/10] revert comment changes and move it to a further PR --- miio/integrations/roidmi/vacuum/roidmivacuum_miot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py index 50cf9425f..fcea53b4d 100644 --- a/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py +++ b/miio/integrations/roidmi/vacuum/roidmivacuum_miot.py @@ -558,6 +558,7 @@ def status(self) -> RoidmiVacuumStatus: return RoidmiVacuumStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None + # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } ) @@ -568,6 +569,7 @@ def consumable_status(self) -> RoidmiConsumableStatus: return RoidmiConsumableStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None + # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } ) @@ -578,6 +580,7 @@ def cleaning_summary(self) -> RoidmiCleaningSummary: return RoidmiCleaningSummary( { prop["did"]: prop["value"] if prop["code"] == 0 else None + # max_properties limmit to 10 to avoid "Checksum error" messages from the device. for prop in self.get_properties_for_mapping() } )