| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172 |
- 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
|