From 6d71c5b2481772958850c26b7a22353efcd19a36 Mon Sep 17 00:00:00 2001 From: ahrmn Date: Tue, 23 Dec 2025 16:43:29 +0100 Subject: [PATCH 01/12] Added settings for Meter Pro Co2 --- switchbot/devices/meter_pro.py | 216 ++++++++++++++++++++++++++++++++- tests/test_meter_pro.py | 53 ++++++++ 2 files changed, 265 insertions(+), 4 deletions(-) diff --git a/switchbot/devices/meter_pro.py b/switchbot/devices/meter_pro.py index 4f4fbbda..ffcfdb7a 100644 --- a/switchbot/devices/meter_pro.py +++ b/switchbot/devices/meter_pro.py @@ -1,19 +1,41 @@ from typing import Any +from bleak import BLEDevice +from .. import SwitchbotModel from ..helpers import parse_uint24_be from .device import SwitchbotDevice, SwitchbotOperationError -COMMAND_SET_TIME_OFFSET = "570f680506" +SETTINGS_HEADER = f"570f68" +COMMAND_SHOW_BATTERY_LEVEL = f"{SETTINGS_HEADER}070108" +COMMAND_DATE_FORMAT = f"{SETTINGS_HEADER}070107" + +COMMAND_TEMPERATURE_UPDATE_INTERVAL = f"{SETTINGS_HEADER}070105" +COMMAND_CO2_UPDATE_INTERVAL = f"{SETTINGS_HEADER}0b06" +COMMAND_FORCE_NEW_CO2_Measurement = f"{SETTINGS_HEADER}0b04" +COMMAND_CO2_THRESHOLDS = f"{SETTINGS_HEADER}020302" +COMMAND_COMFORTLEVEL = f"{SETTINGS_HEADER}020188" + +COMMAND_BUTTON_FUNCTION = f"{SETTINGS_HEADER}070106" +COMMAND_CALIBRATE_CO2_SENSOR = f"{SETTINGS_HEADER}0b02" + +COMMAND_ALERT_SOUND = f"{SETTINGS_HEADER}0204" +COMMAND_ALERT_TEMPERATURE_HUMIDITY = f"570f44" +COMMAND_ALERT_CO2 = f"{SETTINGS_HEADER}020301" + +COMMAND_SET_TIME_OFFSET = f"{SETTINGS_HEADER}0506" COMMAND_GET_TIME_OFFSET = "570f690506" MAX_TIME_OFFSET = (1 << 24) - 1 COMMAND_GET_DEVICE_DATETIME = "570f6901" COMMAND_SET_DEVICE_DATETIME = "57000503" -COMMAND_SET_DISPLAY_FORMAT = "570f680505" - +COMMAND_SET_DISPLAY_FORMAT = f"{SETTINGS_HEADER}0505" class SwitchbotMeterProCO2(SwitchbotDevice): - """API to control Switchbot Meter Pro CO2.""" + """API to control Switchbot Meter Pro CO2. + + The assumptions that the original app has for each value are noted at the respective method. + Which of them are actually required by the device is unknown. + """ async def get_time_offset(self) -> int: """ @@ -157,6 +179,174 @@ async def set_time_display_format(self, is_12h_mode: bool = False) -> None: result = await self._send_command(payload) self._validate_result("set_time_display_format", result) + async def show_battery_level(self, show_battery: bool): + """ Show or hide battery level on the display.""" + show_battery_byte = "01" if show_battery else "00" + await self._send_command(COMMAND_SHOW_BATTERY_LEVEL + show_battery_byte) + + async def set_co2_thresholds(self, lower: int, upper: int): + """ Sets the thresholds to define Air Quality for depiction on display as follows: + co2 < lower => Good (Green) + lower < co2 < upper => Moderate (Orange) + upper < co2 => Poor (Red) + + Original App assumes: + 500 <= lower < upper <= 1900 + lower and upper are multiples of 100 + """ + if lower >= upper: + raise ValueError("Lower should be smaller than upper") + await self._send_command(COMMAND_CO2_THRESHOLDS + f'{lower:04x}' + f'{upper:04x}') + + async def set_comfortlevel(self, cold: float, hot: float, dry: int, wet: int): + """ Sets the Thresholds for comfortable temperature (in C) and humidity to display comfort-level. + The supported values in the original App are as following: + Temperature is -20C to 80C in 0.5C steps + Humidity is 1% to 99% in 1% steps + """ + if cold >= hot: + raise ValueError("Cold should be smaller than Hot") + if dry >= wet: + raise ValueError("Dry should be smaller than Wet") + + point_five = self._get_point_five_byte(cold, hot) + cold_byte = self._encode_temperature(int(cold)) + hot_byte = self._encode_temperature(int(hot)) + + await self._send_command(COMMAND_COMFORTLEVEL + hot_byte + f'{wet:02x}' + point_five + cold_byte + f'{dry:02x}') + + async def set_alert_temperature_humidity(self, + temperature_alert: bool = False, temperature_low: float = -20.0, temperature_high: float = 80.0, temperature_reverse: bool = False, + humidity_alert: bool = False, humidity_low: int = 1, humidity_high: int = 99, humidity_reverse: bool = False, + absolute_humidity_alert: bool = False, absolute_humidity_low: float = 0.00, absolute_humidity_high: float = 99.99, absolute_humidity_reverse: bool = False, + dewpoint_alert: bool = False, dewpoint_low: float = -60.0, dewpoint_high: float = 60.0, dewpoint_reverse: bool = False, + vpd_alert: bool = False, vpd_low: float = 0.00, vpd_high: float = 10.00, vpd_reverse: bool = False + ): + """ + Sets Temperature- and Humidity- related alerts. + *_alert: enable or disable respective alert + *_low and *_high: The respective ranges + *_reverse: If False: Alert if measured value is outside of provided range. + If True: Alert if measured value is inside of provided range. + + The range-boundaries have different assumptions in the original App: + Temperature: Between -20℃ and 80℃ in 0.5℃ steps + Humidity: Between 1% and 99% in 1% steps + Absolute Humidity: Between 0.00g/m^3 and 99.99g/m^3 in 0.5g/m^3 steps (99.99 instead of 100) + Dew Point: Between -60℃ and 60℃ in 0.5℃ steps + VPD: Between 0 kPa and 10 kPa in 0.05 kPa steps + """ + mode_temp_humid = 0x00 + if temperature_alert: + if temperature_reverse: + mode_temp_humid += 0x04 + else: + mode_temp_humid += 0x03 + if humidity_alert: + if humidity_reverse: + mode_temp_humid += 0x40 + else: + mode_temp_humid += 0x30 + + mode_abshumid_dewpoint_vpd = 0x00 + if absolute_humidity_alert: + if absolute_humidity_reverse: + mode_abshumid_dewpoint_vpd += 0x02 + else: + mode_abshumid_dewpoint_vpd += 0x01 + if dewpoint_alert: + if dewpoint_reverse: + mode_abshumid_dewpoint_vpd += 0x10 + else: + mode_abshumid_dewpoint_vpd += 0x0c + if vpd_alert: + if vpd_reverse: + mode_abshumid_dewpoint_vpd += 0x80 + else: + mode_abshumid_dewpoint_vpd += 0x60 + + temperature_point_five = self._get_point_five_byte(temperature_low, temperature_high) + temperature_low_byte = self._encode_temperature(int(temperature_low)) + temperature_high_byte = self._encode_temperature(int(temperature_high)) + + dewpoint_point_five = self._get_point_five_byte(dewpoint_low, dewpoint_high) + dewpoint_low_byte = self._encode_temperature(int(dewpoint_low)) + dewpoint_high_byte = self._encode_temperature(int(dewpoint_high)) + + absolute_humidity_low_bytes = f'{int(absolute_humidity_low):02x}' + f'{int(absolute_humidity_low * 100 % 100):02x}' + absolute_humidity_high_bytes = f'{int(absolute_humidity_high):02x}' + f'{int(absolute_humidity_high * 100 % 100):02x}' + + vpd_bytes = (f'{int(vpd_high * 100 % 100):02x}' + + f'{int(vpd_low * 100 % 100):02x}' + + f'{int(vpd_high):01x}') + f'{int(vpd_low):01x}' + + await self._send_command(COMMAND_ALERT_TEMPERATURE_HUMIDITY + + f'{mode_temp_humid:02x}' + temperature_high_byte + f'{humidity_high:02x}' + temperature_point_five + temperature_low_byte + f'{humidity_low:02x}' + + dewpoint_high_byte + dewpoint_point_five + dewpoint_low_byte + + vpd_bytes + + f'{mode_abshumid_dewpoint_vpd:02x}' + absolute_humidity_high_bytes + absolute_humidity_low_bytes) + + async def set_alert_co2(self, on: bool, co2_low: int, co2_high: max, reverse: bool): + """ Sets the CO2-Alert. + on: Turn CO2-Alert on or off + lower and upper: The provided range (between 400ppm and 2000ppm in 100ppm steps) + reverse: If False: Alert if measured value is outside of provided range. + If True: Alert if measured value is inside of provided range. + """ + if co2_high < co2_low: + raise ValueError("Upper value should bigger than the lower value. Do you want to use reverse instead?") + + mode = 0x00 if not on else ( + 0x04 if reverse else 0x03 + ) + await self._send_command(COMMAND_ALERT_CO2 + f'{mode:02x}' + f'{co2_high:04x}' + f'{co2_low:04x}') + + async def set_temperature_update_interval(self, minutes: int): + """ Sets the interval in which temperature and humidity are measured in battery powered mode. + Original App assumes minutes in {5, 10, 30} + """ + seconds = minutes * 60 + await self._send_command(COMMAND_TEMPERATURE_UPDATE_INTERVAL + f'{seconds:04x}') + + async def set_co2_update_interval(self, minutes: int): + """ Sets the interval in which co2 levels are measured in battery powered mode. + Original App assumes minutes in {5, 10, 30} + """ + seconds = minutes * 60 + await self._send_command(COMMAND_CO2_UPDATE_INTERVAL + f'{seconds:04x}') + + async def set_button_function(self, change_unit: bool, change_data_source: bool): + """ Sets the function of te top button: + Default (both options false): Only update data + changeUnit: switch between ℃ and ℉ + changeDataSource: switch between display of indoor and outdoor temperature + """ + change_unit_byte = '00' if change_unit else '01' # yes, it has to be reversed like this! + change_data_source_byte = '01' if change_data_source else '00' # yes, it has to be reversed like this! + await self._send_command(COMMAND_BUTTON_FUNCTION + change_unit_byte + change_data_source_byte) + + async def force_new_co2_measurement(self): + """ Requests a new CO2 measurement, regardless of update interval """ + await self._send_command(COMMAND_FORCE_NEW_CO2_Measurement) + + async def calibrate_co2_sensor(self): + """ Calibrate CO2-Sensor. + Place your device in a well-ventilated area for 1 minute before calling this. + After calling this the calibration runs for about 5 minutes. + Keep the device still during this process. + """ + await self._send_command(COMMAND_CALIBRATE_CO2_SENSOR) + + async def set_alert_sound(self, sound_on: bool, volume: int): + """ + Sets the Alert-Mode. + If soundOn is False the display flashes. + If soundOn is True the device additionally beeps. + The volume is expected to be in {2,3,4} (2: low, 3: medium, 4: high) + """ + sound_on_byte = '02' if sound_on else '01' + await self._send_command(COMMAND_ALERT_SOUND + f'{volume:02x}' + sound_on_byte) + def _validate_result( self, op_name: str, result: bytes | None, min_length: int | None = None ) -> bytes: @@ -170,3 +360,21 @@ def _validate_result( f"{self.name}: Unexpected response len for {op_name}, wanted at least {min_length} (result={result.hex() if result else 'None'} rssi={self.rssi})" ) return result + + def _get_point_five_byte(self, cold: float, hot: float): + """ This byte represents if either of the temperatures has a .5 decimalplace """ + point_five = 0x00 + if int(cold * 10) % 10 == 5: + point_five += 0x05 + if int(hot * 10) % 10 == 5: + point_five += 0x50 + return f'{point_five:02x}' + + def _encode_temperature(self, temp: int): + # The encoding for a negative temperature is the value as hex + # The encoding for a positive temperature is the value + 128 as hex + if temp > 0: + temp += 128 + else: + temp *= -1 + return f'{temp:02x}' diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 449a8636..85e95338 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -247,3 +247,56 @@ async def test_set_time_display_format_failure(): with pytest.raises(SwitchbotOperationError): await device.set_time_display_format(is_12h_mode=True) + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("show_battery", "expected_payload"), + [ + (True, "01"), + (False, "00"), + ], +) +async def test_show_battery_level(show_battery: bool, expected_payload: str): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.show_battery_level(show_battery=show_battery) + device._send_command.assert_called_with("570f68070108" + expected_payload) + + +@pytest.mark.asyncio +async def test_set_co2_thresholds(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_co2_thresholds(lower=500, upper=1000) + device._send_command.assert_called_with("570f6802030201f403e8") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("cold", "hot", "dry", "wet", "expected_payload"), + [ + (10.0, 20.0, 40, 80, "9450008a28"), + (-20.0, -10.0, 40, 80, "0a50001428"), + (-20.0, 70, 40, 80, "c650001428"), + (0.5, 22, 40, 82, "9652050028"), + (0, 22, 40, 82, "9652000028"), + (14, 37.5, 30, 70, "a546508e1e"), + ], +) +async def test_set_comfortlevel(cold: float, hot: float, dry: int, wet: int, expected_payload: str): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_comfortlevel(cold, hot, dry, wet) + device._send_command.assert_called_with("570f68020188" + expected_payload) + +# todo: test_set_alert_temperature_humidity +# todo: test_set_alert_co2 +# todo: test_set_temperature_update_interval +# todo: test_set_co2_update_interval +# todo: test_set_button_function +# todo: test_force_new_co2_measurement +# todo: test_calibrate_co2_sensor +# todo: test_set_alert_sound \ No newline at end of file From 8c3107f7ba051ce2974214dad8b381dd9bb6ce9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:23:09 +0000 Subject: [PATCH 02/12] chore(pre-commit.ci): auto fixes --- switchbot/devices/meter_pro.py | 179 ++++++++++++++++++++++----------- tests/test_meter_pro.py | 8 +- 2 files changed, 126 insertions(+), 61 deletions(-) diff --git a/switchbot/devices/meter_pro.py b/switchbot/devices/meter_pro.py index ffcfdb7a..cc07792c 100644 --- a/switchbot/devices/meter_pro.py +++ b/switchbot/devices/meter_pro.py @@ -1,11 +1,9 @@ from typing import Any -from bleak import BLEDevice -from .. import SwitchbotModel from ..helpers import parse_uint24_be from .device import SwitchbotDevice, SwitchbotOperationError -SETTINGS_HEADER = f"570f68" +SETTINGS_HEADER = "570f68" COMMAND_SHOW_BATTERY_LEVEL = f"{SETTINGS_HEADER}070108" COMMAND_DATE_FORMAT = f"{SETTINGS_HEADER}070107" @@ -19,7 +17,7 @@ COMMAND_CALIBRATE_CO2_SENSOR = f"{SETTINGS_HEADER}0b02" COMMAND_ALERT_SOUND = f"{SETTINGS_HEADER}0204" -COMMAND_ALERT_TEMPERATURE_HUMIDITY = f"570f44" +COMMAND_ALERT_TEMPERATURE_HUMIDITY = "570f44" COMMAND_ALERT_CO2 = f"{SETTINGS_HEADER}020301" COMMAND_SET_TIME_OFFSET = f"{SETTINGS_HEADER}0506" @@ -30,8 +28,10 @@ COMMAND_SET_DEVICE_DATETIME = "57000503" COMMAND_SET_DISPLAY_FORMAT = f"{SETTINGS_HEADER}0505" + class SwitchbotMeterProCO2(SwitchbotDevice): - """API to control Switchbot Meter Pro CO2. + """ + API to control Switchbot Meter Pro CO2. The assumptions that the original app has for each value are noted at the respective method. Which of them are actually required by the device is unknown. @@ -180,12 +180,13 @@ async def set_time_display_format(self, is_12h_mode: bool = False) -> None: self._validate_result("set_time_display_format", result) async def show_battery_level(self, show_battery: bool): - """ Show or hide battery level on the display.""" + """Show or hide battery level on the display.""" show_battery_byte = "01" if show_battery else "00" await self._send_command(COMMAND_SHOW_BATTERY_LEVEL + show_battery_byte) async def set_co2_thresholds(self, lower: int, upper: int): - """ Sets the thresholds to define Air Quality for depiction on display as follows: + """ + Sets the thresholds to define Air Quality for depiction on display as follows: co2 < lower => Good (Green) lower < co2 < upper => Moderate (Orange) upper < co2 => Poor (Red) @@ -196,10 +197,13 @@ async def set_co2_thresholds(self, lower: int, upper: int): """ if lower >= upper: raise ValueError("Lower should be smaller than upper") - await self._send_command(COMMAND_CO2_THRESHOLDS + f'{lower:04x}' + f'{upper:04x}') + await self._send_command( + COMMAND_CO2_THRESHOLDS + f"{lower:04x}" + f"{upper:04x}" + ) async def set_comfortlevel(self, cold: float, hot: float, dry: int, wet: int): - """ Sets the Thresholds for comfortable temperature (in C) and humidity to display comfort-level. + """ + Sets the Thresholds for comfortable temperature (in C) and humidity to display comfort-level. The supported values in the original App are as following: Temperature is -20C to 80C in 0.5C steps Humidity is 1% to 99% in 1% steps @@ -213,14 +217,37 @@ async def set_comfortlevel(self, cold: float, hot: float, dry: int, wet: int): cold_byte = self._encode_temperature(int(cold)) hot_byte = self._encode_temperature(int(hot)) - await self._send_command(COMMAND_COMFORTLEVEL + hot_byte + f'{wet:02x}' + point_five + cold_byte + f'{dry:02x}') + await self._send_command( + COMMAND_COMFORTLEVEL + + hot_byte + + f"{wet:02x}" + + point_five + + cold_byte + + f"{dry:02x}" + ) - async def set_alert_temperature_humidity(self, - temperature_alert: bool = False, temperature_low: float = -20.0, temperature_high: float = 80.0, temperature_reverse: bool = False, - humidity_alert: bool = False, humidity_low: int = 1, humidity_high: int = 99, humidity_reverse: bool = False, - absolute_humidity_alert: bool = False, absolute_humidity_low: float = 0.00, absolute_humidity_high: float = 99.99, absolute_humidity_reverse: bool = False, - dewpoint_alert: bool = False, dewpoint_low: float = -60.0, dewpoint_high: float = 60.0, dewpoint_reverse: bool = False, - vpd_alert: bool = False, vpd_low: float = 0.00, vpd_high: float = 10.00, vpd_reverse: bool = False + async def set_alert_temperature_humidity( + self, + temperature_alert: bool = False, + temperature_low: float = -20.0, + temperature_high: float = 80.0, + temperature_reverse: bool = False, + humidity_alert: bool = False, + humidity_low: int = 1, + humidity_high: int = 99, + humidity_reverse: bool = False, + absolute_humidity_alert: bool = False, + absolute_humidity_low: float = 0.00, + absolute_humidity_high: float = 99.99, + absolute_humidity_reverse: bool = False, + dewpoint_alert: bool = False, + dewpoint_low: float = -60.0, + dewpoint_high: float = 60.0, + dewpoint_reverse: bool = False, + vpd_alert: bool = False, + vpd_low: float = 0.00, + vpd_high: float = 10.00, + vpd_reverse: bool = False, ): """ Sets Temperature- and Humidity- related alerts. @@ -258,14 +285,16 @@ async def set_alert_temperature_humidity(self, if dewpoint_reverse: mode_abshumid_dewpoint_vpd += 0x10 else: - mode_abshumid_dewpoint_vpd += 0x0c + mode_abshumid_dewpoint_vpd += 0x0C if vpd_alert: if vpd_reverse: mode_abshumid_dewpoint_vpd += 0x80 else: mode_abshumid_dewpoint_vpd += 0x60 - temperature_point_five = self._get_point_five_byte(temperature_low, temperature_high) + temperature_point_five = self._get_point_five_byte( + temperature_low, temperature_high + ) temperature_low_byte = self._encode_temperature(int(temperature_low)) temperature_high_byte = self._encode_temperature(int(temperature_high)) @@ -273,64 +302,96 @@ async def set_alert_temperature_humidity(self, dewpoint_low_byte = self._encode_temperature(int(dewpoint_low)) dewpoint_high_byte = self._encode_temperature(int(dewpoint_high)) - absolute_humidity_low_bytes = f'{int(absolute_humidity_low):02x}' + f'{int(absolute_humidity_low * 100 % 100):02x}' - absolute_humidity_high_bytes = f'{int(absolute_humidity_high):02x}' + f'{int(absolute_humidity_high * 100 % 100):02x}' - - vpd_bytes = (f'{int(vpd_high * 100 % 100):02x}' - + f'{int(vpd_low * 100 % 100):02x}' - + f'{int(vpd_high):01x}') + f'{int(vpd_low):01x}' + absolute_humidity_low_bytes = ( + f"{int(absolute_humidity_low):02x}" + + f"{int(absolute_humidity_low * 100 % 100):02x}" + ) + absolute_humidity_high_bytes = ( + f"{int(absolute_humidity_high):02x}" + + f"{int(absolute_humidity_high * 100 % 100):02x}" + ) - await self._send_command(COMMAND_ALERT_TEMPERATURE_HUMIDITY - + f'{mode_temp_humid:02x}' + temperature_high_byte + f'{humidity_high:02x}' + temperature_point_five + temperature_low_byte + f'{humidity_low:02x}' - + dewpoint_high_byte + dewpoint_point_five + dewpoint_low_byte - + vpd_bytes - + f'{mode_abshumid_dewpoint_vpd:02x}' + absolute_humidity_high_bytes + absolute_humidity_low_bytes) + vpd_bytes = ( + f"{int(vpd_high * 100 % 100):02x}" + + f"{int(vpd_low * 100 % 100):02x}" + + f"{int(vpd_high):01x}" + ) + f"{int(vpd_low):01x}" + + await self._send_command( + COMMAND_ALERT_TEMPERATURE_HUMIDITY + + f"{mode_temp_humid:02x}" + + temperature_high_byte + + f"{humidity_high:02x}" + + temperature_point_five + + temperature_low_byte + + f"{humidity_low:02x}" + + dewpoint_high_byte + + dewpoint_point_five + + dewpoint_low_byte + + vpd_bytes + + f"{mode_abshumid_dewpoint_vpd:02x}" + + absolute_humidity_high_bytes + + absolute_humidity_low_bytes + ) async def set_alert_co2(self, on: bool, co2_low: int, co2_high: max, reverse: bool): - """ Sets the CO2-Alert. - on: Turn CO2-Alert on or off - lower and upper: The provided range (between 400ppm and 2000ppm in 100ppm steps) - reverse: If False: Alert if measured value is outside of provided range. - If True: Alert if measured value is inside of provided range. - """ + """ + Sets the CO2-Alert. + on: Turn CO2-Alert on or off + lower and upper: The provided range (between 400ppm and 2000ppm in 100ppm steps) + reverse: If False: Alert if measured value is outside of provided range. + If True: Alert if measured value is inside of provided range. + """ if co2_high < co2_low: - raise ValueError("Upper value should bigger than the lower value. Do you want to use reverse instead?") + raise ValueError( + "Upper value should bigger than the lower value. Do you want to use reverse instead?" + ) - mode = 0x00 if not on else ( - 0x04 if reverse else 0x03 + mode = 0x00 if not on else (0x04 if reverse else 0x03) + await self._send_command( + COMMAND_ALERT_CO2 + f"{mode:02x}" + f"{co2_high:04x}" + f"{co2_low:04x}" ) - await self._send_command(COMMAND_ALERT_CO2 + f'{mode:02x}' + f'{co2_high:04x}' + f'{co2_low:04x}') async def set_temperature_update_interval(self, minutes: int): - """ Sets the interval in which temperature and humidity are measured in battery powered mode. - Original App assumes minutes in {5, 10, 30} - """ + """ + Sets the interval in which temperature and humidity are measured in battery powered mode. + Original App assumes minutes in {5, 10, 30} + """ seconds = minutes * 60 - await self._send_command(COMMAND_TEMPERATURE_UPDATE_INTERVAL + f'{seconds:04x}') + await self._send_command(COMMAND_TEMPERATURE_UPDATE_INTERVAL + f"{seconds:04x}") async def set_co2_update_interval(self, minutes: int): - """ Sets the interval in which co2 levels are measured in battery powered mode. - Original App assumes minutes in {5, 10, 30} - """ + """ + Sets the interval in which co2 levels are measured in battery powered mode. + Original App assumes minutes in {5, 10, 30} + """ seconds = minutes * 60 - await self._send_command(COMMAND_CO2_UPDATE_INTERVAL + f'{seconds:04x}') + await self._send_command(COMMAND_CO2_UPDATE_INTERVAL + f"{seconds:04x}") async def set_button_function(self, change_unit: bool, change_data_source: bool): - """ Sets the function of te top button: + """ + Sets the function of te top button: Default (both options false): Only update data changeUnit: switch between ℃ and ℉ changeDataSource: switch between display of indoor and outdoor temperature - """ - change_unit_byte = '00' if change_unit else '01' # yes, it has to be reversed like this! - change_data_source_byte = '01' if change_data_source else '00' # yes, it has to be reversed like this! - await self._send_command(COMMAND_BUTTON_FUNCTION + change_unit_byte + change_data_source_byte) + """ + change_unit_byte = ( + "00" if change_unit else "01" + ) # yes, it has to be reversed like this! + change_data_source_byte = ( + "01" if change_data_source else "00" + ) # yes, it has to be reversed like this! + await self._send_command( + COMMAND_BUTTON_FUNCTION + change_unit_byte + change_data_source_byte + ) async def force_new_co2_measurement(self): - """ Requests a new CO2 measurement, regardless of update interval """ + """Requests a new CO2 measurement, regardless of update interval""" await self._send_command(COMMAND_FORCE_NEW_CO2_Measurement) async def calibrate_co2_sensor(self): - """ Calibrate CO2-Sensor. + """ + Calibrate CO2-Sensor. Place your device in a well-ventilated area for 1 minute before calling this. After calling this the calibration runs for about 5 minutes. Keep the device still during this process. @@ -344,8 +405,8 @@ async def set_alert_sound(self, sound_on: bool, volume: int): If soundOn is True the device additionally beeps. The volume is expected to be in {2,3,4} (2: low, 3: medium, 4: high) """ - sound_on_byte = '02' if sound_on else '01' - await self._send_command(COMMAND_ALERT_SOUND + f'{volume:02x}' + sound_on_byte) + sound_on_byte = "02" if sound_on else "01" + await self._send_command(COMMAND_ALERT_SOUND + f"{volume:02x}" + sound_on_byte) def _validate_result( self, op_name: str, result: bytes | None, min_length: int | None = None @@ -362,13 +423,13 @@ def _validate_result( return result def _get_point_five_byte(self, cold: float, hot: float): - """ This byte represents if either of the temperatures has a .5 decimalplace """ + """This byte represents if either of the temperatures has a .5 decimalplace""" point_five = 0x00 if int(cold * 10) % 10 == 5: point_five += 0x05 if int(hot * 10) % 10 == 5: point_five += 0x50 - return f'{point_five:02x}' + return f"{point_five:02x}" def _encode_temperature(self, temp: int): # The encoding for a negative temperature is the value as hex @@ -377,4 +438,4 @@ def _encode_temperature(self, temp: int): temp += 128 else: temp *= -1 - return f'{temp:02x}' + return f"{temp:02x}" diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 85e95338..efc61d92 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -248,6 +248,7 @@ async def test_set_time_display_format_failure(): with pytest.raises(SwitchbotOperationError): await device.set_time_display_format(is_12h_mode=True) + @pytest.mark.asyncio @pytest.mark.parametrize( ("show_battery", "expected_payload"), @@ -285,13 +286,16 @@ async def test_set_co2_thresholds(): (14, 37.5, 30, 70, "a546508e1e"), ], ) -async def test_set_comfortlevel(cold: float, hot: float, dry: int, wet: int, expected_payload: str): +async def test_set_comfortlevel( + cold: float, hot: float, dry: int, wet: int, expected_payload: str +): device = create_device() device._send_command.return_value = bytes.fromhex("01") await device.set_comfortlevel(cold, hot, dry, wet) device._send_command.assert_called_with("570f68020188" + expected_payload) + # todo: test_set_alert_temperature_humidity # todo: test_set_alert_co2 # todo: test_set_temperature_update_interval @@ -299,4 +303,4 @@ async def test_set_comfortlevel(cold: float, hot: float, dry: int, wet: int, exp # todo: test_set_button_function # todo: test_force_new_co2_measurement # todo: test_calibrate_co2_sensor -# todo: test_set_alert_sound \ No newline at end of file +# todo: test_set_alert_sound From 4b307e7a463fd3ce4dced9edf4a7c7c407327b15 Mon Sep 17 00:00:00 2001 From: ahrmn Date: Tue, 27 Jan 2026 23:00:23 +0100 Subject: [PATCH 03/12] Feedback and Tests --- switchbot/devices/meter_pro.py | 10 +-- tests/test_meter_pro.py | 160 +++++++++++++++++++++++++++++++-- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/switchbot/devices/meter_pro.py b/switchbot/devices/meter_pro.py index cc07792c..db2610c1 100644 --- a/switchbot/devices/meter_pro.py +++ b/switchbot/devices/meter_pro.py @@ -9,7 +9,7 @@ COMMAND_TEMPERATURE_UPDATE_INTERVAL = f"{SETTINGS_HEADER}070105" COMMAND_CO2_UPDATE_INTERVAL = f"{SETTINGS_HEADER}0b06" -COMMAND_FORCE_NEW_CO2_Measurement = f"{SETTINGS_HEADER}0b04" +COMMAND_FORCE_NEW_CO2_MEASUREMENT = f"{SETTINGS_HEADER}0b04" COMMAND_CO2_THRESHOLDS = f"{SETTINGS_HEADER}020302" COMMAND_COMFORTLEVEL = f"{SETTINGS_HEADER}020188" @@ -334,7 +334,7 @@ async def set_alert_temperature_humidity( + absolute_humidity_low_bytes ) - async def set_alert_co2(self, on: bool, co2_low: int, co2_high: max, reverse: bool): + async def set_alert_co2(self, on: bool, co2_low: int, co2_high: int, reverse: bool): """ Sets the CO2-Alert. on: Turn CO2-Alert on or off @@ -370,7 +370,7 @@ async def set_co2_update_interval(self, minutes: int): async def set_button_function(self, change_unit: bool, change_data_source: bool): """ - Sets the function of te top button: + Sets the function of the top button: Default (both options false): Only update data changeUnit: switch between ℃ and ℉ changeDataSource: switch between display of indoor and outdoor temperature @@ -387,7 +387,7 @@ async def set_button_function(self, change_unit: bool, change_data_source: bool) async def force_new_co2_measurement(self): """Requests a new CO2 measurement, regardless of update interval""" - await self._send_command(COMMAND_FORCE_NEW_CO2_Measurement) + await self._send_command(COMMAND_FORCE_NEW_CO2_MEASUREMENT) async def calibrate_co2_sensor(self): """ @@ -423,7 +423,7 @@ def _validate_result( return result def _get_point_five_byte(self, cold: float, hot: float): - """This byte represents if either of the temperatures has a .5 decimalplace""" + """Represents if either of the temperatures has a .5 decimalplace """ point_five = 0x00 if int(cold * 10) % 10 == 5: point_five += 0x05 diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index efc61d92..89d68321 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -296,11 +296,155 @@ async def test_set_comfortlevel( device._send_command.assert_called_with("570f68020188" + expected_payload) -# todo: test_set_alert_temperature_humidity -# todo: test_set_alert_co2 -# todo: test_set_temperature_update_interval -# todo: test_set_co2_update_interval -# todo: test_set_button_function -# todo: test_force_new_co2_measurement -# todo: test_calibrate_co2_sensor -# todo: test_set_alert_sound +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("temperature_alert", "temperature_low", "temperature_high", "temperature_reverse", + "humidity_alert", "humidity_low", "humidity_high", "humidity_reverse", + "absolute_humidity_alert", "absolute_humidity_low", "absolute_humidity_high", "absolute_humidity_reverse", + "dewpoint_alert", "dewpoint_low", "dewpoint_high", "dewpoint_reverse", + "vpd_alert", "vpd_low", "vpd_high", "vpd_reverse", + "expected_payload"), + [ + (True, -20, 80, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04d063001401bc003c0000a00063630000"), + (True, -20, 59.5, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04bb63501401bc003c0000a00063630000"), + (True, -11.5, 59.5, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04bb63550b01bc003c0000a00063630000"), + (True, -11.5, 59.5, False, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "03bb63550b01bc003c0000a00063630000"), + (True, -11.5, 59.5, False, True, 20, 80, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "33bb50550b14bc003c0000a00063630000"), + (True, -11.5, 59.5, False, True, 20, 80, True, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "43bb50550b14bc003c0000a00063630000"), + (False, -11.5, 59.5, False, True, 20, 80, True, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "40bb50550b14bc003c0000a00063630000"), + (False, -11.5, 59.5, False, False, 20, 80, False, True, 15.00, 70.00, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "00bb50550b14bc003c0000a00146000f00"), + (False, -11.5, 59.5, False, False, 20, 80, False, True, 17.50, 69.50, True, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "00bb50550b14bc003c0000a00245321132"), + (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -47.0, 41.0, False, False, 0.00, 10.00, False, "00bb50550b14a9002f0000a00c63630600"), + (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -47.0, 41.0, True, False, 0.00, 10.00, False, "00bb50550b14a9002f0000a01063630600"), + (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -15.5, 42.5, True, False, 0.00, 10.00, False, "00bb50550b14aa550f0000a01063630600"), + (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, False, -15.5, 42.5, True, True, 0.00, 10.00, False, "00bb50550b14aa550f0000a06063630600"), + (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, False, -15.5, 42.5, True, True, 1.05, 8.75, True, "00bb50550b14aa550f4b05818063630600"), + + + ], +) +async def test_set_alert_temperature_humidity(temperature_alert: bool , temperature_low: float, temperature_high: float, temperature_reverse: bool, + humidity_alert: bool, humidity_low: int, humidity_high: int, humidity_reverse: bool, + absolute_humidity_alert: bool, absolute_humidity_low: float, absolute_humidity_high: float, absolute_humidity_reverse: bool, + dewpoint_alert: bool, dewpoint_low: float, dewpoint_high: float, dewpoint_reverse: bool, + vpd_alert: bool, vpd_low: float, vpd_high: float, vpd_reverse: bool, expected_payload: str): + # Values based on actual measurements from the app + + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_alert_temperature_humidity(temperature_alert, temperature_low, temperature_high, temperature_reverse, humidity_alert, humidity_low, humidity_high, humidity_reverse, absolute_humidity_alert, absolute_humidity_low, absolute_humidity_high, absolute_humidity_reverse, dewpoint_alert, dewpoint_low, dewpoint_high, dewpoint_reverse, vpd_alert, vpd_low, vpd_high, vpd_reverse) + device._send_command.assert_called_with("570f44" + expected_payload) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("on", "co2_low", "co2_high", "reverse", "expected_payload"), + [ + (False, 1000, 2000, False, "0007d003e8"), + (True, 1000, 2000, False, "0307d003e8"), + (True, 700, 2000, False, "0307d002bc"), + (True, 700, 1500, False, "0305dc02bc"), + (True, 700, 1500, True, "0405dc02bc"), + ], +) +async def test_set_alert_co2(on: bool, co2_low: int, co2_high: int, reverse: bool, expected_payload: str): + # Values based on actual measurements from the app + + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_alert_co2(on, co2_low, co2_high, reverse) + device._send_command.assert_called_with("570f68020301" + expected_payload) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("minutes", "expected_payload"), + [ + (5, "012c"), + (10, "0258"), + (30, "0708"), + ], +) +async def test_set_temperature_update_interval(minutes: int, expected_payload: str): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_temperature_update_interval(minutes) + device._send_command.assert_called_with("570f68070105" + expected_payload) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("minutes", "expected_payload"), + [ + (5, "012c"), + (10, "0258"), + (30, "0708"), + ], +) +async def test_set_co2_update_interval(minutes: int, expected_payload: str): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_co2_update_interval(minutes) + device._send_command.assert_called_with("570f680b06" + expected_payload) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("change_unit", "change_data_source", "expected_payload"), + [ + (True, True, "0001"), + (True, False, "0000"), + (False, True, "0101"), + (False, False, "0100"), + ], +) +async def test_set_button_function(change_unit: bool, change_data_source: bool, expected_payload: str): + # Values based on actual measurements from the app + + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_button_function(change_unit, change_data_source) + device._send_command.assert_called_with("570f68070106" + expected_payload) + + +@pytest.mark.asyncio +async def test_force_new_co2_measurement(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.force_new_co2_measurement() + device._send_command.assert_called_with("570f680b04") + + +@pytest.mark.asyncio +async def test_calibrate_co2_sensor(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.calibrate_co2_sensor() + device._send_command.assert_called_with("570f680b02") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("sound_on", "volume", "expected_payload"), + [ + (False, 4, "0401"), + (True, 2, "0202"), + (True, 3, "0302"), + (True, 4, "0402"), + ], +) +async def test_set_alert_sound(sound_on: bool, volume: int, expected_payload: str): + # Values based on actual measurements from the app + + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + await device.set_alert_sound(sound_on, volume) + device._send_command.assert_called_with("570f680204" + expected_payload) From 8b987bab5e50eac2192e78061b483051ef44adaa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:02:59 +0000 Subject: [PATCH 04/12] chore(pre-commit.ci): auto fixes --- switchbot/devices/meter_pro.py | 2 +- tests/test_meter_pro.py | 444 ++++++++++++++++++++++++++++++--- 2 files changed, 406 insertions(+), 40 deletions(-) diff --git a/switchbot/devices/meter_pro.py b/switchbot/devices/meter_pro.py index db2610c1..fe340657 100644 --- a/switchbot/devices/meter_pro.py +++ b/switchbot/devices/meter_pro.py @@ -423,7 +423,7 @@ def _validate_result( return result def _get_point_five_byte(self, cold: float, hot: float): - """Represents if either of the temperatures has a .5 decimalplace """ + """Represents if either of the temperatures has a .5 decimalplace""" point_five = 0x00 if int(cold * 10) % 10 == 5: point_five += 0x05 diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 89d68321..21bf24b9 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -298,42 +298,404 @@ async def test_set_comfortlevel( @pytest.mark.asyncio @pytest.mark.parametrize( - ("temperature_alert", "temperature_low", "temperature_high", "temperature_reverse", - "humidity_alert", "humidity_low", "humidity_high", "humidity_reverse", - "absolute_humidity_alert", "absolute_humidity_low", "absolute_humidity_high", "absolute_humidity_reverse", - "dewpoint_alert", "dewpoint_low", "dewpoint_high", "dewpoint_reverse", - "vpd_alert", "vpd_low", "vpd_high", "vpd_reverse", - "expected_payload"), + ( + "temperature_alert", + "temperature_low", + "temperature_high", + "temperature_reverse", + "humidity_alert", + "humidity_low", + "humidity_high", + "humidity_reverse", + "absolute_humidity_alert", + "absolute_humidity_low", + "absolute_humidity_high", + "absolute_humidity_reverse", + "dewpoint_alert", + "dewpoint_low", + "dewpoint_high", + "dewpoint_reverse", + "vpd_alert", + "vpd_low", + "vpd_high", + "vpd_reverse", + "expected_payload", + ), [ - (True, -20, 80, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04d063001401bc003c0000a00063630000"), - (True, -20, 59.5, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04bb63501401bc003c0000a00063630000"), - (True, -11.5, 59.5, True, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "04bb63550b01bc003c0000a00063630000"), - (True, -11.5, 59.5, False, False, 1, 99, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "03bb63550b01bc003c0000a00063630000"), - (True, -11.5, 59.5, False, True, 20, 80, False, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "33bb50550b14bc003c0000a00063630000"), - (True, -11.5, 59.5, False, True, 20, 80, True, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "43bb50550b14bc003c0000a00063630000"), - (False, -11.5, 59.5, False, True, 20, 80, True, False, 0.00, 99.99, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "40bb50550b14bc003c0000a00063630000"), - (False, -11.5, 59.5, False, False, 20, 80, False, True, 15.00, 70.00, False, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "00bb50550b14bc003c0000a00146000f00"), - (False, -11.5, 59.5, False, False, 20, 80, False, True, 17.50, 69.50, True, False, -60.0, 60.0, False, False, 0.00, 10.00, False, "00bb50550b14bc003c0000a00245321132"), - (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -47.0, 41.0, False, False, 0.00, 10.00, False, "00bb50550b14a9002f0000a00c63630600"), - (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -47.0, 41.0, True, False, 0.00, 10.00, False, "00bb50550b14a9002f0000a01063630600"), - (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, True, -15.5, 42.5, True, False, 0.00, 10.00, False, "00bb50550b14aa550f0000a01063630600"), - (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, False, -15.5, 42.5, True, True, 0.00, 10.00, False, "00bb50550b14aa550f0000a06063630600"), - (False, -11.5, 59.5, False, False, 20, 80, False, False, 06.00, 99.99, False, False, -15.5, 42.5, True, True, 1.05, 8.75, True, "00bb50550b14aa550f4b05818063630600"), - - + ( + True, + -20, + 80, + True, + False, + 1, + 99, + False, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "04d063001401bc003c0000a00063630000", + ), + ( + True, + -20, + 59.5, + True, + False, + 1, + 99, + False, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "04bb63501401bc003c0000a00063630000", + ), + ( + True, + -11.5, + 59.5, + True, + False, + 1, + 99, + False, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "04bb63550b01bc003c0000a00063630000", + ), + ( + True, + -11.5, + 59.5, + False, + False, + 1, + 99, + False, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "03bb63550b01bc003c0000a00063630000", + ), + ( + True, + -11.5, + 59.5, + False, + True, + 20, + 80, + False, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "33bb50550b14bc003c0000a00063630000", + ), + ( + True, + -11.5, + 59.5, + False, + True, + 20, + 80, + True, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "43bb50550b14bc003c0000a00063630000", + ), + ( + False, + -11.5, + 59.5, + False, + True, + 20, + 80, + True, + False, + 0.00, + 99.99, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "40bb50550b14bc003c0000a00063630000", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + True, + 15.00, + 70.00, + False, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "00bb50550b14bc003c0000a00146000f00", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + True, + 17.50, + 69.50, + True, + False, + -60.0, + 60.0, + False, + False, + 0.00, + 10.00, + False, + "00bb50550b14bc003c0000a00245321132", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + False, + 06.00, + 99.99, + False, + True, + -47.0, + 41.0, + False, + False, + 0.00, + 10.00, + False, + "00bb50550b14a9002f0000a00c63630600", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + False, + 06.00, + 99.99, + False, + True, + -47.0, + 41.0, + True, + False, + 0.00, + 10.00, + False, + "00bb50550b14a9002f0000a01063630600", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + False, + 06.00, + 99.99, + False, + True, + -15.5, + 42.5, + True, + False, + 0.00, + 10.00, + False, + "00bb50550b14aa550f0000a01063630600", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + False, + 06.00, + 99.99, + False, + False, + -15.5, + 42.5, + True, + True, + 0.00, + 10.00, + False, + "00bb50550b14aa550f0000a06063630600", + ), + ( + False, + -11.5, + 59.5, + False, + False, + 20, + 80, + False, + False, + 06.00, + 99.99, + False, + False, + -15.5, + 42.5, + True, + True, + 1.05, + 8.75, + True, + "00bb50550b14aa550f4b05818063630600", + ), ], ) -async def test_set_alert_temperature_humidity(temperature_alert: bool , temperature_low: float, temperature_high: float, temperature_reverse: bool, - humidity_alert: bool, humidity_low: int, humidity_high: int, humidity_reverse: bool, - absolute_humidity_alert: bool, absolute_humidity_low: float, absolute_humidity_high: float, absolute_humidity_reverse: bool, - dewpoint_alert: bool, dewpoint_low: float, dewpoint_high: float, dewpoint_reverse: bool, - vpd_alert: bool, vpd_low: float, vpd_high: float, vpd_reverse: bool, expected_payload: str): +async def test_set_alert_temperature_humidity( + temperature_alert: bool, + temperature_low: float, + temperature_high: float, + temperature_reverse: bool, + humidity_alert: bool, + humidity_low: int, + humidity_high: int, + humidity_reverse: bool, + absolute_humidity_alert: bool, + absolute_humidity_low: float, + absolute_humidity_high: float, + absolute_humidity_reverse: bool, + dewpoint_alert: bool, + dewpoint_low: float, + dewpoint_high: float, + dewpoint_reverse: bool, + vpd_alert: bool, + vpd_low: float, + vpd_high: float, + vpd_reverse: bool, + expected_payload: str, +): # Values based on actual measurements from the app device = create_device() device._send_command.return_value = bytes.fromhex("01") - await device.set_alert_temperature_humidity(temperature_alert, temperature_low, temperature_high, temperature_reverse, humidity_alert, humidity_low, humidity_high, humidity_reverse, absolute_humidity_alert, absolute_humidity_low, absolute_humidity_high, absolute_humidity_reverse, dewpoint_alert, dewpoint_low, dewpoint_high, dewpoint_reverse, vpd_alert, vpd_low, vpd_high, vpd_reverse) + await device.set_alert_temperature_humidity( + temperature_alert, + temperature_low, + temperature_high, + temperature_reverse, + humidity_alert, + humidity_low, + humidity_high, + humidity_reverse, + absolute_humidity_alert, + absolute_humidity_low, + absolute_humidity_high, + absolute_humidity_reverse, + dewpoint_alert, + dewpoint_low, + dewpoint_high, + dewpoint_reverse, + vpd_alert, + vpd_low, + vpd_high, + vpd_reverse, + ) device._send_command.assert_called_with("570f44" + expected_payload) @@ -342,13 +704,15 @@ async def test_set_alert_temperature_humidity(temperature_alert: bool , temperat ("on", "co2_low", "co2_high", "reverse", "expected_payload"), [ (False, 1000, 2000, False, "0007d003e8"), - (True, 1000, 2000, False, "0307d003e8"), - (True, 700, 2000, False, "0307d002bc"), - (True, 700, 1500, False, "0305dc02bc"), - (True, 700, 1500, True, "0405dc02bc"), + (True, 1000, 2000, False, "0307d003e8"), + (True, 700, 2000, False, "0307d002bc"), + (True, 700, 1500, False, "0305dc02bc"), + (True, 700, 1500, True, "0405dc02bc"), ], ) -async def test_set_alert_co2(on: bool, co2_low: int, co2_high: int, reverse: bool, expected_payload: str): +async def test_set_alert_co2( + on: bool, co2_low: int, co2_high: int, reverse: bool, expected_payload: str +): # Values based on actual measurements from the app device = create_device() @@ -362,7 +726,7 @@ async def test_set_alert_co2(on: bool, co2_low: int, co2_high: int, reverse: boo @pytest.mark.parametrize( ("minutes", "expected_payload"), [ - (5, "012c"), + (5, "012c"), (10, "0258"), (30, "0708"), ], @@ -379,7 +743,7 @@ async def test_set_temperature_update_interval(minutes: int, expected_payload: s @pytest.mark.parametrize( ("minutes", "expected_payload"), [ - (5, "012c"), + (5, "012c"), (10, "0258"), (30, "0708"), ], @@ -396,13 +760,15 @@ async def test_set_co2_update_interval(minutes: int, expected_payload: str): @pytest.mark.parametrize( ("change_unit", "change_data_source", "expected_payload"), [ - (True, True, "0001"), - (True, False, "0000"), - (False, True, "0101"), + (True, True, "0001"), + (True, False, "0000"), + (False, True, "0101"), (False, False, "0100"), ], ) -async def test_set_button_function(change_unit: bool, change_data_source: bool, expected_payload: str): +async def test_set_button_function( + change_unit: bool, change_data_source: bool, expected_payload: str +): # Values based on actual measurements from the app device = create_device() From 8803eb4d72c5938edd6ff6eac2d689669516b1c0 Mon Sep 17 00:00:00 2001 From: ahrmn Date: Thu, 19 Mar 2026 20:34:38 +0100 Subject: [PATCH 05/12] Removed Humidity and Temperature Alert --- switchbot/devices/meter_pro.py | 108 --------- tests/test_meter_pro.py | 403 --------------------------------- 2 files changed, 511 deletions(-) diff --git a/switchbot/devices/meter_pro.py b/switchbot/devices/meter_pro.py index fe340657..6b34ed71 100644 --- a/switchbot/devices/meter_pro.py +++ b/switchbot/devices/meter_pro.py @@ -226,114 +226,6 @@ async def set_comfortlevel(self, cold: float, hot: float, dry: int, wet: int): + f"{dry:02x}" ) - async def set_alert_temperature_humidity( - self, - temperature_alert: bool = False, - temperature_low: float = -20.0, - temperature_high: float = 80.0, - temperature_reverse: bool = False, - humidity_alert: bool = False, - humidity_low: int = 1, - humidity_high: int = 99, - humidity_reverse: bool = False, - absolute_humidity_alert: bool = False, - absolute_humidity_low: float = 0.00, - absolute_humidity_high: float = 99.99, - absolute_humidity_reverse: bool = False, - dewpoint_alert: bool = False, - dewpoint_low: float = -60.0, - dewpoint_high: float = 60.0, - dewpoint_reverse: bool = False, - vpd_alert: bool = False, - vpd_low: float = 0.00, - vpd_high: float = 10.00, - vpd_reverse: bool = False, - ): - """ - Sets Temperature- and Humidity- related alerts. - *_alert: enable or disable respective alert - *_low and *_high: The respective ranges - *_reverse: If False: Alert if measured value is outside of provided range. - If True: Alert if measured value is inside of provided range. - - The range-boundaries have different assumptions in the original App: - Temperature: Between -20℃ and 80℃ in 0.5℃ steps - Humidity: Between 1% and 99% in 1% steps - Absolute Humidity: Between 0.00g/m^3 and 99.99g/m^3 in 0.5g/m^3 steps (99.99 instead of 100) - Dew Point: Between -60℃ and 60℃ in 0.5℃ steps - VPD: Between 0 kPa and 10 kPa in 0.05 kPa steps - """ - mode_temp_humid = 0x00 - if temperature_alert: - if temperature_reverse: - mode_temp_humid += 0x04 - else: - mode_temp_humid += 0x03 - if humidity_alert: - if humidity_reverse: - mode_temp_humid += 0x40 - else: - mode_temp_humid += 0x30 - - mode_abshumid_dewpoint_vpd = 0x00 - if absolute_humidity_alert: - if absolute_humidity_reverse: - mode_abshumid_dewpoint_vpd += 0x02 - else: - mode_abshumid_dewpoint_vpd += 0x01 - if dewpoint_alert: - if dewpoint_reverse: - mode_abshumid_dewpoint_vpd += 0x10 - else: - mode_abshumid_dewpoint_vpd += 0x0C - if vpd_alert: - if vpd_reverse: - mode_abshumid_dewpoint_vpd += 0x80 - else: - mode_abshumid_dewpoint_vpd += 0x60 - - temperature_point_five = self._get_point_five_byte( - temperature_low, temperature_high - ) - temperature_low_byte = self._encode_temperature(int(temperature_low)) - temperature_high_byte = self._encode_temperature(int(temperature_high)) - - dewpoint_point_five = self._get_point_five_byte(dewpoint_low, dewpoint_high) - dewpoint_low_byte = self._encode_temperature(int(dewpoint_low)) - dewpoint_high_byte = self._encode_temperature(int(dewpoint_high)) - - absolute_humidity_low_bytes = ( - f"{int(absolute_humidity_low):02x}" - + f"{int(absolute_humidity_low * 100 % 100):02x}" - ) - absolute_humidity_high_bytes = ( - f"{int(absolute_humidity_high):02x}" - + f"{int(absolute_humidity_high * 100 % 100):02x}" - ) - - vpd_bytes = ( - f"{int(vpd_high * 100 % 100):02x}" - + f"{int(vpd_low * 100 % 100):02x}" - + f"{int(vpd_high):01x}" - ) + f"{int(vpd_low):01x}" - - await self._send_command( - COMMAND_ALERT_TEMPERATURE_HUMIDITY - + f"{mode_temp_humid:02x}" - + temperature_high_byte - + f"{humidity_high:02x}" - + temperature_point_five - + temperature_low_byte - + f"{humidity_low:02x}" - + dewpoint_high_byte - + dewpoint_point_five - + dewpoint_low_byte - + vpd_bytes - + f"{mode_abshumid_dewpoint_vpd:02x}" - + absolute_humidity_high_bytes - + absolute_humidity_low_bytes - ) - async def set_alert_co2(self, on: bool, co2_low: int, co2_high: int, reverse: bool): """ Sets the CO2-Alert. diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 21bf24b9..b2e8eed8 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -296,409 +296,6 @@ async def test_set_comfortlevel( device._send_command.assert_called_with("570f68020188" + expected_payload) -@pytest.mark.asyncio -@pytest.mark.parametrize( - ( - "temperature_alert", - "temperature_low", - "temperature_high", - "temperature_reverse", - "humidity_alert", - "humidity_low", - "humidity_high", - "humidity_reverse", - "absolute_humidity_alert", - "absolute_humidity_low", - "absolute_humidity_high", - "absolute_humidity_reverse", - "dewpoint_alert", - "dewpoint_low", - "dewpoint_high", - "dewpoint_reverse", - "vpd_alert", - "vpd_low", - "vpd_high", - "vpd_reverse", - "expected_payload", - ), - [ - ( - True, - -20, - 80, - True, - False, - 1, - 99, - False, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "04d063001401bc003c0000a00063630000", - ), - ( - True, - -20, - 59.5, - True, - False, - 1, - 99, - False, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "04bb63501401bc003c0000a00063630000", - ), - ( - True, - -11.5, - 59.5, - True, - False, - 1, - 99, - False, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "04bb63550b01bc003c0000a00063630000", - ), - ( - True, - -11.5, - 59.5, - False, - False, - 1, - 99, - False, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "03bb63550b01bc003c0000a00063630000", - ), - ( - True, - -11.5, - 59.5, - False, - True, - 20, - 80, - False, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "33bb50550b14bc003c0000a00063630000", - ), - ( - True, - -11.5, - 59.5, - False, - True, - 20, - 80, - True, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "43bb50550b14bc003c0000a00063630000", - ), - ( - False, - -11.5, - 59.5, - False, - True, - 20, - 80, - True, - False, - 0.00, - 99.99, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "40bb50550b14bc003c0000a00063630000", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - True, - 15.00, - 70.00, - False, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "00bb50550b14bc003c0000a00146000f00", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - True, - 17.50, - 69.50, - True, - False, - -60.0, - 60.0, - False, - False, - 0.00, - 10.00, - False, - "00bb50550b14bc003c0000a00245321132", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - False, - 06.00, - 99.99, - False, - True, - -47.0, - 41.0, - False, - False, - 0.00, - 10.00, - False, - "00bb50550b14a9002f0000a00c63630600", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - False, - 06.00, - 99.99, - False, - True, - -47.0, - 41.0, - True, - False, - 0.00, - 10.00, - False, - "00bb50550b14a9002f0000a01063630600", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - False, - 06.00, - 99.99, - False, - True, - -15.5, - 42.5, - True, - False, - 0.00, - 10.00, - False, - "00bb50550b14aa550f0000a01063630600", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - False, - 06.00, - 99.99, - False, - False, - -15.5, - 42.5, - True, - True, - 0.00, - 10.00, - False, - "00bb50550b14aa550f0000a06063630600", - ), - ( - False, - -11.5, - 59.5, - False, - False, - 20, - 80, - False, - False, - 06.00, - 99.99, - False, - False, - -15.5, - 42.5, - True, - True, - 1.05, - 8.75, - True, - "00bb50550b14aa550f4b05818063630600", - ), - ], -) -async def test_set_alert_temperature_humidity( - temperature_alert: bool, - temperature_low: float, - temperature_high: float, - temperature_reverse: bool, - humidity_alert: bool, - humidity_low: int, - humidity_high: int, - humidity_reverse: bool, - absolute_humidity_alert: bool, - absolute_humidity_low: float, - absolute_humidity_high: float, - absolute_humidity_reverse: bool, - dewpoint_alert: bool, - dewpoint_low: float, - dewpoint_high: float, - dewpoint_reverse: bool, - vpd_alert: bool, - vpd_low: float, - vpd_high: float, - vpd_reverse: bool, - expected_payload: str, -): - # Values based on actual measurements from the app - - device = create_device() - device._send_command.return_value = bytes.fromhex("01") - - await device.set_alert_temperature_humidity( - temperature_alert, - temperature_low, - temperature_high, - temperature_reverse, - humidity_alert, - humidity_low, - humidity_high, - humidity_reverse, - absolute_humidity_alert, - absolute_humidity_low, - absolute_humidity_high, - absolute_humidity_reverse, - dewpoint_alert, - dewpoint_low, - dewpoint_high, - dewpoint_reverse, - vpd_alert, - vpd_low, - vpd_high, - vpd_reverse, - ) - device._send_command.assert_called_with("570f44" + expected_payload) - - @pytest.mark.asyncio @pytest.mark.parametrize( ("on", "co2_low", "co2_high", "reverse", "expected_payload"), From 6800eda8948f7ce477792f272d291801979f9cdc Mon Sep 17 00:00:00 2001 From: ahrmn Date: Thu, 19 Mar 2026 20:51:41 +0100 Subject: [PATCH 06/12] Covererage --- tests/test_meter_pro.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index b2e8eed8..16e55fc3 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -273,6 +273,10 @@ async def test_set_co2_thresholds(): await device.set_co2_thresholds(lower=500, upper=1000) device._send_command.assert_called_with("570f6802030201f403e8") + # Error if lower >= upper + with pytest.raises(ValueError): + await device.set_co2_thresholds(lower=500, upper=400) + @pytest.mark.asyncio @pytest.mark.parametrize( @@ -296,6 +300,20 @@ async def test_set_comfortlevel( device._send_command.assert_called_with("570f68020188" + expected_payload) +@pytest.mark.asyncio +async def test_set_alert_co2_throws_on_invalid_input(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + # Error if cold >= hot + with pytest.raises(ValueError): + await device.set_comfortlevel(16, 15, 10, 20) + + # Error if dry >= wet + with pytest.raises(ValueError): + await device.set_comfortlevel(15, 16, 20, 10) + + @pytest.mark.asyncio @pytest.mark.parametrize( ("on", "co2_low", "co2_high", "reverse", "expected_payload"), @@ -319,6 +337,16 @@ async def test_set_alert_co2( device._send_command.assert_called_with("570f68020301" + expected_payload) +@pytest.mark.asyncio +async def test_set_alert_co2_throws_on_invalid_input(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + + # Error if lower >= upper + with pytest.raises(ValueError): + await device.set_alert_co2(True, 500, 400, True) + + @pytest.mark.asyncio @pytest.mark.parametrize( ("minutes", "expected_payload"), From 8c3362aed197971f5117370eb09a2b3d8190aad5 Mon Sep 17 00:00:00 2001 From: ahrmn Date: Thu, 19 Mar 2026 20:55:26 +0100 Subject: [PATCH 07/12] Match Errormessage --- tests/test_meter_pro.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 16e55fc3..0dfa3f09 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -274,7 +274,7 @@ async def test_set_co2_thresholds(): device._send_command.assert_called_with("570f6802030201f403e8") # Error if lower >= upper - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Lower should be smaller than upper"): await device.set_co2_thresholds(lower=500, upper=400) @@ -306,11 +306,11 @@ async def test_set_alert_co2_throws_on_invalid_input(): device._send_command.return_value = bytes.fromhex("01") # Error if cold >= hot - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Cold should be smaller than Hot"): await device.set_comfortlevel(16, 15, 10, 20) # Error if dry >= wet - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Dry should be smaller than Wet"): await device.set_comfortlevel(15, 16, 20, 10) @@ -343,7 +343,7 @@ async def test_set_alert_co2_throws_on_invalid_input(): device._send_command.return_value = bytes.fromhex("01") # Error if lower >= upper - with pytest.raises(ValueError): + with pytest.raises(ValueError, match= "Upper value should bigger than the lower value. Do you want to use reverse instead?"): await device.set_alert_co2(True, 500, 400, True) From d53823d0cb2246f64312a14dd83fe04c86eaf676 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:55:40 +0000 Subject: [PATCH 08/12] chore(pre-commit.ci): auto fixes --- tests/test_meter_pro.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 0dfa3f09..6cf711ac 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -343,7 +343,10 @@ async def test_set_alert_co2_throws_on_invalid_input(): device._send_command.return_value = bytes.fromhex("01") # Error if lower >= upper - with pytest.raises(ValueError, match= "Upper value should bigger than the lower value. Do you want to use reverse instead?"): + with pytest.raises( + ValueError, + match="Upper value should bigger than the lower value. Do you want to use reverse instead?", + ): await device.set_alert_co2(True, 500, 400, True) From 05dfc778e895f57871e4ab62bb358d5645f7f962 Mon Sep 17 00:00:00 2001 From: ahrmn Date: Thu, 19 Mar 2026 20:58:36 +0100 Subject: [PATCH 09/12] renamed tests --- tests/test_meter_pro.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 6cf711ac..992ec4a5 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -273,6 +273,11 @@ async def test_set_co2_thresholds(): await device.set_co2_thresholds(lower=500, upper=1000) device._send_command.assert_called_with("570f6802030201f403e8") +@pytest.mark.asyncio +async def test_set_co2_thresholds_throws_on_invalid_input(): + device = create_device() + device._send_command.return_value = bytes.fromhex("01") + # Error if lower >= upper with pytest.raises(ValueError, match="Lower should be smaller than upper"): await device.set_co2_thresholds(lower=500, upper=400) @@ -301,7 +306,7 @@ async def test_set_comfortlevel( @pytest.mark.asyncio -async def test_set_alert_co2_throws_on_invalid_input(): +async def test_set_comfortlevel_throws_on_invalid_input(): device = create_device() device._send_command.return_value = bytes.fromhex("01") From d9a585e65f1c4aa204fb4ea7b3d1658ced3b4fee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:58:49 +0000 Subject: [PATCH 10/12] chore(pre-commit.ci): auto fixes --- tests/test_meter_pro.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 992ec4a5..531cf1f7 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -273,6 +273,7 @@ async def test_set_co2_thresholds(): await device.set_co2_thresholds(lower=500, upper=1000) device._send_command.assert_called_with("570f6802030201f403e8") + @pytest.mark.asyncio async def test_set_co2_thresholds_throws_on_invalid_input(): device = create_device() From d2620f6753f92e38491053e3bc801afeb3de644e Mon Sep 17 00:00:00 2001 From: ahrmn Date: Thu, 19 Mar 2026 20:59:58 +0100 Subject: [PATCH 11/12] escape --- tests/test_meter_pro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 531cf1f7..5a09b71a 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -351,7 +351,7 @@ async def test_set_alert_co2_throws_on_invalid_input(): # Error if lower >= upper with pytest.raises( ValueError, - match="Upper value should bigger than the lower value. Do you want to use reverse instead?", + match="Upper value should bigger than the lower value. Do you want to use reverse instead\?", ): await device.set_alert_co2(True, 500, 400, True) From 8091cd706753bd5c0e43a4ae17691e6a14a543cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:00:42 +0000 Subject: [PATCH 12/12] chore(pre-commit.ci): auto fixes --- tests/test_meter_pro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_meter_pro.py b/tests/test_meter_pro.py index 5a09b71a..2f537814 100644 --- a/tests/test_meter_pro.py +++ b/tests/test_meter_pro.py @@ -351,7 +351,7 @@ async def test_set_alert_co2_throws_on_invalid_input(): # Error if lower >= upper with pytest.raises( ValueError, - match="Upper value should bigger than the lower value. Do you want to use reverse instead\?", + match=r"Upper value should bigger than the lower value. Do you want to use reverse instead\?", ): await device.set_alert_co2(True, 500, 400, True)