-
Notifications
You must be signed in to change notification settings - Fork 62
Control datetime on SwitchBot Meter Pro CO2 Monitor #433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
a07fff1
Adds a class to control SwitchBot Meter Pro (CO2-Monitor) device.
elgris 22bcda0
Implement get- and set- time offset API.
elgris 7aaa0ef
Implement getting the currently displayed datetime info from the Swit…
elgris 6898782
Implement setting the datetime on SwitchbotMeterProCO2 device.
elgris 6c2c6f7
Implement setting 12h(AM/PM) or 24h time display format for Switchbot…
elgris 0b4a4c3
Moved constants and tests to the top level.
elgris 4dcd253
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] f4c4ebc
Merge branch 'master' into co2_meter_time_control
elgris d6fc35c
Move the doc comment from COMMAND_SET_TIME_OFFSET to the method.
elgris 8b6b304
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] 653e212
Comment and code formatting fixes after a round of self-review, also …
elgris 086428c
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] d2c498a
Use a helper for parsing 24 bit ints, instead of manual bit ops.
elgris a4aa2c7
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] cebb619
Move SwitchbotMeterProCO2 to a separate file.
elgris 6488f54
Merge branch 'master' into co2_meter_time_control
bdraco e97bc40
Addresses code review comments: makes set_datetime low-level, adds re…
elgris 4065730
Merge branch 'master' into co2_meter_time_control
elgris 9b9d4ed
Fixes formatting, comment location and turns more tests into paramete…
elgris 9a668bb
Merge branch 'master' into co2_meter_time_control
elgris 09f2605
Merge branch 'master' into co2_meter_time_control
bdraco 38f8994
avoid mixing concat + format strings
bdraco 44e5628
fix utc_offset_minutes bug
bdraco 7404e58
typing
bdraco b4eb33f
tweak 0 handing
bdraco afa1719
address remaining comments from bot
bdraco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| from typing import Any | ||
|
|
||
| from ..helpers import parse_uint24_be | ||
| from .device import SwitchbotDevice, SwitchbotOperationError | ||
|
|
||
| COMMAND_SET_TIME_OFFSET = "570f680506" | ||
| 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" | ||
|
|
||
|
|
||
| class SwitchbotMeterProCO2(SwitchbotDevice): | ||
| """API to control Switchbot Meter Pro CO2.""" | ||
|
|
||
| async def get_time_offset(self) -> int: | ||
| """ | ||
| Get the current display time offset from the device. | ||
|
|
||
| Returns: | ||
| int: The time offset in seconds. Max 24 bits. | ||
|
|
||
| """ | ||
| # Response Format: 5 bytes, where | ||
| # - byte 0: "01" (success) | ||
| # - byte 1: "00" (plus offset) or "80" (minus offset) | ||
| # - bytes 2-4: int24, number of seconds to offset. | ||
| # Example response: 01-80-00-10-00 -> subtract 4096 seconds. | ||
| result = await self._send_command(COMMAND_GET_TIME_OFFSET) | ||
| result = self._validate_result("get_time_offset", result, min_length=5) | ||
|
|
||
| is_negative = bool(result[1] & 0b10000000) | ||
| offset = parse_uint24_be(result, 2) | ||
| return -offset if is_negative else offset | ||
|
|
||
| async def set_time_offset(self, offset_seconds: int) -> None: | ||
| """ | ||
| Set the display time offset on the device. This is what happens when | ||
| you adjust display time in the Switchbot app. The displayed time is | ||
| calculated as the internal device time (usually comes from the factory | ||
| settings or set by the Switchbot app upon syncing) + offset. The offset | ||
| is provided in seconds and can be positive or negative. | ||
|
|
||
| Args: | ||
| offset_seconds (int): 2^24 maximum, can be negative. | ||
|
|
||
| """ | ||
| abs_offset = abs(offset_seconds) | ||
| if abs_offset > MAX_TIME_OFFSET: | ||
| raise SwitchbotOperationError( | ||
| f"{self.name}: Requested to set_time_offset of {offset_seconds} seconds, allowed +-{MAX_TIME_OFFSET} max." | ||
| ) | ||
|
|
||
| sign_byte = "80" if offset_seconds < 0 else "00" | ||
|
|
||
| # Example: 57-0f-68-05-06-80-00-10-00 -> subtract 4096 seconds. | ||
| payload = f"{COMMAND_SET_TIME_OFFSET}{sign_byte}{abs_offset:06x}" | ||
| result = await self._send_command(payload) | ||
| self._validate_result("set_time_offset", result) | ||
|
|
||
| async def get_datetime(self) -> dict[str, Any]: | ||
| """ | ||
| Get the current device time and settings as it is displayed. Contains | ||
| a time offset, if any was applied (see set_time_offset). | ||
| Doesn't include the current time zone. | ||
|
|
||
| Returns: | ||
| dict: Dictionary containing: | ||
| - 12h_mode (bool): True if 12h mode, False if 24h mode. | ||
| - year (int) | ||
| - month (int) | ||
| - day (int) | ||
| - hour (int) | ||
| - minute (int) | ||
| - second (int) | ||
|
|
||
| """ | ||
| # Response Format: 13 bytes, where | ||
| # - byte 0: "01" (success) | ||
| # - bytes 1-4: temperature, ignored here. | ||
| # - byte 5: time display format: | ||
| # - "80" - 12h (am/pm) | ||
| # - "00" - 24h | ||
| # - bytes 6-12: yyyy-MM-dd-hh-mm-ss | ||
| # Example: 01-e4-02-94-23-00-07-e9-0c-1e-08-37-01 contains | ||
| # "year 2025, 30 December, 08:55:01, displayed in 24h format". | ||
| result = await self._send_command(COMMAND_GET_DEVICE_DATETIME) | ||
| result = self._validate_result("get_datetime", result, min_length=13) | ||
| return { | ||
| # Whether the time is displayed in 12h(am/pm) or 24h mode. | ||
| "12h_mode": bool(result[5] & 0b10000000), | ||
| "year": (result[6] << 8) + result[7], | ||
| "month": result[8], | ||
| "day": result[9], | ||
| "hour": result[10], | ||
| "minute": result[11], | ||
| "second": result[12], | ||
| } | ||
|
|
||
| async def set_datetime( | ||
| self, timestamp: int, utc_offset_hours: int = 0, utc_offset_minutes: int = 0 | ||
| ) -> None: | ||
| """ | ||
| Set the device internal time and timezone. Similar to how the | ||
| Switchbot app does it upon syncing with the device. | ||
| Pay attention to calculating UTC offset hours and minutes, see | ||
| examples below. | ||
|
|
||
| Args: | ||
| timestamp (int): Unix timestamp in seconds. | ||
| utc_offset_hours (int): UTC offset in hours, floor()'ed, | ||
| within [-12; 14] range. | ||
| Examples: -5 for UTC-05:00, -6 for UTC-05:30, | ||
| 5 for UTC+05:00, 5 for UTC+5:30. | ||
| utc_offset_minutes (int): UTC offset minutes component, always | ||
| positive, complements utc_offset_hours. | ||
| Examples: 45 for UTC+05:45, 15 for UTC-5:45. | ||
|
|
||
| """ | ||
| if not (-12 <= utc_offset_hours <= 14): | ||
| raise SwitchbotOperationError( | ||
| f"{self.name}: utc_offset_hours must be between -12 and +14 inclusive, got {utc_offset_hours}" | ||
| ) | ||
| if not (0 <= utc_offset_minutes < 60): | ||
| raise SwitchbotOperationError( | ||
| f"{self.name}: utc_offset_minutes must be between 0 and 59 inclusive, got {utc_offset_minutes}" | ||
| ) | ||
|
|
||
| # The device doesn't automatically add offset minutes, it expects them | ||
| # to come as a part of the timestamp. | ||
| adjusted_timestamp = timestamp + utc_offset_minutes * 60 | ||
|
|
||
| # The timezone is encoded as 1 byte, where 00 stands for UTC-12. | ||
| # TZ with minute offset gets floor()ed: 4:30 yields 4, -4:30 yields -5. | ||
| utc_byte = utc_offset_hours + 12 | ||
|
|
||
| payload = ( | ||
| f"{COMMAND_SET_DEVICE_DATETIME}{utc_byte:02x}" | ||
| f"{adjusted_timestamp:016x}{utc_offset_minutes:02x}" | ||
| ) | ||
|
|
||
| result = await self._send_command(payload) | ||
| self._validate_result("set_datetime", result) | ||
|
|
||
| async def set_time_display_format(self, is_12h_mode: bool = False) -> None: | ||
| """ | ||
| Set the time display format on the device: 12h(AM/PM) or 24h. | ||
|
|
||
| Args: | ||
| is_12h_mode (bool): True for 12h (AM/PM) mode, False for 24h mode. | ||
|
|
||
| """ | ||
| mode_byte = "80" if is_12h_mode else "00" | ||
| payload = f"{COMMAND_SET_DISPLAY_FORMAT}{mode_byte}" | ||
| result = await self._send_command(payload) | ||
| self._validate_result("set_time_display_format", result) | ||
|
|
||
| def _validate_result( | ||
| self, op_name: str, result: bytes | None, min_length: int | None = None | ||
| ) -> bytes: | ||
| if not self._check_command_result(result, 0, {1}): | ||
| raise SwitchbotOperationError( | ||
| f"{self.name}: Unexpected response code for {op_name} (result={result.hex() if result else 'None'} rssi={self.rssi})" | ||
| ) | ||
| assert result is not None | ||
| if min_length is not None and len(result) < min_length: | ||
| raise SwitchbotOperationError( | ||
| 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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.