From d7ffb89ef2761fd797ab5f5935891440d0dbd31d Mon Sep 17 00:00:00 2001 From: david Date: Fri, 29 Aug 2025 16:50:55 +1000 Subject: [PATCH] Permit setting aqi_realtime_update_duration and do not override the value of aqi_realtime_update_duration if it is set for the Mi Air Purifier 3/3H (zhimi.airpurifier.mb3). Tested against a 3H. Signed-off-by: david --- .../zhimi/airpurifier/airpurifier_miot.py | 47 +++++++++++++++---- .../tests/test_airpurifier_miot.py | 43 +++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py index 46e48408e..5d7d2feb1 100644 --- a/miio/integrations/zhimi/airpurifier/airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/airpurifier_miot.py @@ -334,7 +334,8 @@ class AirPurifierMiotStatus(DeviceStatus): {'did': 'average_aqi', 'siid': 13, 'piid': 2, 'code': 0, 'value': 2}, {'did': 'filter_rfid_tag', 'siid': 14, 'piid': 1, 'code': 0, 'value': '81:6b:3f:32:84:4b:4'}, {'did': 'filter_rfid_product_id', 'siid': 14, 'piid': 3, 'code': 0, 'value': '0:0:31:31'}, - {'did': 'app_extra', 'siid': 15, 'piid': 1, 'code': 0, 'value': 0} + {'did': 'app_extra', 'siid': 15, 'piid': 1, 'code': 0, 'value': 0}, + {'did': 'aqi_realtime_update_duration', 'siid': 13, 'piid': 9, 'code': 0, 'value': 0} ] """ @@ -520,6 +521,11 @@ def gestures(self) -> Optional[bool]: """Return True if gesture control is on.""" return self.data.get("gestures") + @property + def aqi_realtime_update_duration(self) -> Optional[int]: + """Return the aqi_realtime_update_duration in use.""" + return self.data.get("aqi_realtime_update_duration") + class AirPurifierMiot(MiotDevice): """Main class representing the air purifier which uses MIoT protocol.""" @@ -555,24 +561,30 @@ class AirPurifierMiot(MiotDevice): "Motor speed: {result.motor_speed} rpm\n" "Filter RFID product id: {result.filter_rfid_product_id}\n" "Filter RFID tag: {result.filter_rfid_tag}\n" - "Filter type: {result.filter_type}\n", + "Filter type: {result.filter_type}\n" + "AQI Update Duration: {result.aqi_realtime_update_duration}\n", ) ) def status(self) -> AirPurifierMiotStatus: """Retrieve properties.""" - # Some devices update the aqi information only every 30min. - # This forces the device to poll the sensor for 5 seconds, - # so that we get always the most recent values. See #1281. - if self.model == "zhimi.airpurifier.mb3": - self.set_property("aqi_realtime_update_duration", 5) - - return AirPurifierMiotStatus( + status = AirPurifierMiotStatus( { prop["did"]: prop["value"] if prop["code"] == 0 else None for prop in self.get_properties_for_mapping() }, self.model, ) + if ( + self.model == "zhimi.airpurifier.mb3" + and not status.aqi_realtime_update_duration + ): + # Some devices update the aqi information only every 30min. + # This forces the device to poll the sensor for 5 seconds, + # so that we get always the most recent values. See #1281. + self.set_aqi_realtime_update_duration(5) + status.data["aqi_realtime_update_duration"] = 5 + + return status @command(default_output=format_output("Powering on")) def on(self): @@ -764,3 +776,20 @@ def set_led_brightness_level(self, level: int): raise ValueError("Invalid brightness level: %s" % level) return self.set_property("led_brightness_level", level) + + @command( + click.argument("duration", type=int), + default_output=format_output( + "Setting AQI Realtime update duration to {duration}" + ), + ) + def set_aqi_realtime_update_duration(self, duration: int): + """Set the AQI Realtime update duration.""" + if "aqi_realtime_update_duration" not in self._get_mapping(): + raise UnsupportedFeatureException( + "Unsupported aqi_realtime_update_duration for model '%s'" % self.model + ) + + if duration < 0: + raise ValueError("Invalid aqi realtime update duration: %s" % duration) + return self.set_property("aqi_realtime_update_duration", duration) diff --git a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py index b4b4411c3..ba8159b92 100644 --- a/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py +++ b/miio/integrations/zhimi/airpurifier/tests/test_airpurifier_miot.py @@ -31,8 +31,10 @@ "filter_rfid_product_id": "0:0:41:30", "filter_rfid_tag": "10:20:30:40:50:60:7", "button_pressed": "power", + "aqi_realtime_update_duration": 0, } + _INITIAL_STATE_MB4 = { "power": True, "aqi": 10, @@ -91,6 +93,9 @@ def __init__(self, *args, **kwargs): ), "set_act_det": lambda x: self._set_state("act_det", x), "set_app_extra": lambda x: self._set_state("app_extra", x), + "set_aqi_realtime_update_duration": lambda x: self._set_state( + "aqi_realtime_update_duration", x + ), } super().__init__(*args, **kwargs) @@ -236,6 +241,44 @@ def test_set_anion(self): self.device.set_anion(True) +class DummyAirPurifierMiotMB3(DummyAirPurifierMiot): + def __init__(self, *args, **kwargs): + self._model = "zhimi.airpurifier.mb3" + super().__init__(*args, **kwargs) + + +@pytest.fixture(scope="function") +def airpurifierMB3(request): + request.cls.device = DummyAirPurifierMiotMB3() + + +@pytest.mark.usefixtures("airpurifierMB3") +class TestAirPurifierMB3(TestCase): + def test_status(self): + default_adjusted_value = 5 + specific_value = 10 + status = self.device.status() + assert _INITIAL_STATE["aqi_realtime_update_duration"] == 0 + assert status.aqi_realtime_update_duration == default_adjusted_value + + # Set a specific value + + self.device.set_aqi_realtime_update_duration(specific_value) + + # Check that we do not override a specific set value + status = self.device.status() + + assert status.aqi_realtime_update_duration == specific_value + + def test_set_aqi_realtime_update_duration_negative_value(self): + with pytest.raises(ValueError): + self.device.set_aqi_realtime_update_duration(-1) + + def test_set_aqi_realtime_update_duration(self): + self.device.set_aqi_realtime_update_duration(12) + assert self.device.status().aqi_realtime_update_duration == 12 + + class DummyAirPurifierMiotMB4(DummyAirPurifierMiot): def __init__(self, *args, **kwargs): self._model = "zhimi.airpurifier.mb4"