Skip to content

Added settings for Meter Pro Co2#430

Open
ahrmn wants to merge 14 commits intosblibs:masterfrom
ahrmn:master
Open

Added settings for Meter Pro Co2#430
ahrmn wants to merge 14 commits intosblibs:masterfrom
ahrmn:master

Conversation

@ahrmn
Copy link
Copy Markdown

@ahrmn ahrmn commented Dec 23, 2025

I reverse-engineered the settings of the Meter Pro (CO2) and implemented them here.
I have no means of testing if this work for the other Meter models.
An example script to fully configure a device using this would be for example

import asyncio

from switchbot.devices.meter import SwitchbotMeter
from switchbot.discovery import GetSwitchbotDevices
from switchbot.const import SwitchbotModel

BLE_MAC='XX:XX:XX:XX:XX:XX'
METER_MODEL=SwitchbotModel.METER_PRO_C

async def main():
    switchbotDevices = await GetSwitchbotDevices().discover()
    meter_advertisement = switchbotDevices[BLE_MAC]
    print(meter_advertisement.data)
    meter = SwitchbotMeter(device=meter_advertisement.device, model=METER_MODEL)
    await meter.show_battery_level(False)
    await meter.set_co2_thresholds(900, 1200)
    await meter.set_comfortlevel(19, 22, 40, 70)
    await meter.set_temperature_update_interval(10)
    await meter.set_co2_update_interval(10)
    await meter.set_button_function(False, False)
    await meter.set_alert_sound(False, 2)
    await meter.set_alert_co2(True, 400, 1200, False)
    
asyncio.run(main())

Additionally one can trigger a new measurement regardless of the frequency using
await meter.force_new_CO2_measurement() and calibrate the sensor (at fresh air!) using
await meter.calibrate_co2_sensor().

@ahrmn
Copy link
Copy Markdown
Author

ahrmn commented Dec 23, 2025

This may solve #416.
@jacekowski Can you test if this synchronises time for the Meter Pro (without CO2 sensor)?

Comment thread switchbot/devices/meter.py Outdated
@zerzhang
Copy link
Copy Markdown
Collaborator

zerzhang commented Jan 7, 2026

Additional unit tests are needed. It is recommended to create a base class MeterPro and move the CO2 settings to a separate class MeterProCO2.

@ahrmn ahrmn reopened this Jan 25, 2026
@ahrmn
Copy link
Copy Markdown
Author

ahrmn commented Jan 25, 2026

@elgris @zerzhang I integrated my code into the new structure, removed my own time-implementation and started implementing unit-tests (more are coming when i have more time).

It is recommended to create a base class MeterPro and move the CO2 settings to a separate class MeterProCO2.

I have no way to verify which methods would actually work on a MeterPro, so i am not sure abbout that.

Comment thread switchbot/devices/meter_pro.py Outdated

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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(style) it should be all uppercase COMMAND_FORCE_NEW_CO2_MEASUREMENT.

Comment thread switchbot/devices/meter_pro.py Outdated
+ absolute_humidity_low_bytes
)

async def set_alert_co2(self, on: bool, co2_low: int, co2_high: max, reverse: bool):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: co2_high: int,

Comment thread switchbot/devices/meter_pro.py

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}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to send these limits if on is False? I haven't tried this with the device, but I'd assume that it expects all zeroes. If so, then we can probably make set default values for co2_high and co2_low args to 0 and drop their validation when on is False.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this would be a reasonable design, but is not what the original app does.
The test I pushed for this method contains only payloads from the original App.
When a mode gets turned off, all values remain set as they were before. And all of them get transmitted.
I tried to stick as close to this as possible. I did not test how the device would react to your proposal.

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
Copy link
Copy Markdown
Contributor

@elgris elgris Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to @zerzhang and @bdraco, but I'd just pass seconds as an arg and let the caller do the minutes->seconds conversion. The same for other _update_interval methods.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it is recommended to use seconds as the parameter.

Comment thread switchbot/devices/meter_pro.py Outdated

async def set_button_function(self, change_unit: bool, change_data_source: bool):
"""
Sets the function of te top button:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: te -> the

@elgris
Copy link
Copy Markdown
Contributor

elgris commented Jan 26, 2026

I'd also suggest checking ruff output, it also found some problems. You can also run it locally to verify that everything looks good.

@ahrmn
Copy link
Copy Markdown
Author

ahrmn commented Jan 27, 2026

I added more tests and fixed the typos you found.
After the Auto-Fixes I find some of the test parameters rather hard to read....

@ahrmn
Copy link
Copy Markdown
Author

ahrmn commented Feb 12, 2026

@zerzhang Could you provide me with some guidance, what actions are required to move this forward?
I see that the CI is red, but currently dont see a nice way to solve its problems.

@elgris
Copy link
Copy Markdown
Contributor

elgris commented Feb 18, 2026

@ahrmn : maybe remove set_alert_temperature_humidity from this PR for now? We can approach it separately and iteratively. Also smaller PRs are easier to review IMO. If you ask me, I'd actually split set_alert_temperature_humidity into smaller focused functions, like set_alert_temperature, set_alert_humidity and so on.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/devices/meter_pro.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ahrmn
Copy link
Copy Markdown
Author

ahrmn commented Apr 19, 2026

@zerzhang May I ask what is blocking this?
I removed the Humidity- and Temperature alerts that caused problems with the CI and added more test-coverage.

@zerzhang
Copy link
Copy Markdown
Collaborator

@zerzhang May I ask what is blocking this? I removed the Humidity- and Temperature alerts that caused problems with the CI and added more test-coverage.
Meter Pro .xlsx

@ahrmn Here is the latest communication protocol file; you can make adjustments based on it.

Comment thread switchbot/devices/meter.py Outdated
Comment on lines +60 to +63
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 super()._send_command(COMMAND_SHOW_BATTERY_LEVEL + show_battery_byte)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

00 indicates that the battery icon will be displayed when the battery level is <= 20%, 01 indicates the default display.

Comment thread switchbot/devices/meter.py Outdated
await super()._send_command(
COMMAND_COMFORTLEVEL
+ hot_byte
+ f"{wet:02x}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit7 is reserved, bit6-0 represent the humidity.

Comment thread switchbot/devices/meter.py Outdated
point_five += 0x05
if int(hot * 10) % 10 == 5:
point_five += 0x50
return f"{point_five:02x}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def get_temp_decimal_byte(cold: float, hot: float) -> int:
    cold_dec = int(round(cold * 10)) % 10
    hot_dec = int(round(hot * 10)) % 10

    if not (0 <= cold_dec <= 9 and 0 <= hot_dec <= 9):
        raise ValueError("Decimal part out of range")

    return (hot_dec << 4) | cold_dec

"Temperature ValueH decimal
bit [7-4]: Temperature ValueH decimal, upper limit of temperature alarm, decimal part
Temperature ValueL decimal
bit [3-0]: Temperature ValueL decimal, lower limit of temperature alarm, decimal part
0000 –1001: 0~9"

This section itself supports setting decimal numbers from 0 to 9.

Comment thread switchbot/devices/meter.py Outdated
+ f"{wet:02x}"
+ point_five
+ cold_byte
+ f"{dry:02x}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as wet byte

Comment thread switchbot/devices/meter.py Outdated
await super()._send_command(
COMMAND_ALERT_SOUND + f"{volume:02x}" + sound_on_byte
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alarm Volume
0xFF -hold
0x01 Off
0x02 Low
0x03 Medium (Default)
0x04 High

Audible and visual alarm mode
0x00 - disable
0x01 - Flashing only
0x02 - Sound + flashing

seconds = minutes * 60
await self._send_command(COMMAND_TEMPERATURE_UPDATE_INTERVAL + f"{seconds:04x}")

async def set_co2_update_interval(self, minutes: int):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

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):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

COMMAND_BUTTON_FUNCTION + change_unit_byte + change_data_source_byte
)

async def force_new_co2_measurement(self):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

"""Requests a new CO2 measurement, regardless of update interval"""
await self._send_command(COMMAND_FORCE_NEW_CO2_MEASUREMENT)

async def calibrate_co2_sensor(self):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

"""
await self._send_command(COMMAND_CALIBRATE_CO2_SENSOR)

async def set_alert_sound(self, sound_on: bool, volume: int):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing return type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants