meter_pro.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. from typing import Any
  2. from ..helpers import parse_uint24_be
  3. from .device import SwitchbotDevice, SwitchbotOperationError
  4. COMMAND_SET_TIME_OFFSET = "570f680506"
  5. COMMAND_GET_TIME_OFFSET = "570f690506"
  6. MAX_TIME_OFFSET = (1 << 24) - 1
  7. COMMAND_GET_DEVICE_DATETIME = "570f6901"
  8. COMMAND_SET_DEVICE_DATETIME = "57000503"
  9. COMMAND_SET_DISPLAY_FORMAT = "570f680505"
  10. class SwitchbotMeterProCO2(SwitchbotDevice):
  11. """API to control Switchbot Meter Pro CO2."""
  12. async def get_time_offset(self) -> int:
  13. """
  14. Get the current display time offset from the device.
  15. Returns:
  16. int: The time offset in seconds. Max 24 bits.
  17. """
  18. # Response Format: 5 bytes, where
  19. # - byte 0: "01" (success)
  20. # - byte 1: "00" (plus offset) or "80" (minus offset)
  21. # - bytes 2-4: int24, number of seconds to offset.
  22. # Example response: 01-80-00-10-00 -> subtract 4096 seconds.
  23. result = await self._send_command(COMMAND_GET_TIME_OFFSET)
  24. result = self._validate_result("get_time_offset", result, min_length=5)
  25. is_negative = bool(result[1] & 0b10000000)
  26. offset = parse_uint24_be(result, 2)
  27. return -offset if is_negative else offset
  28. async def set_time_offset(self, offset_seconds: int) -> None:
  29. """
  30. Set the display time offset on the device. This is what happens when
  31. you adjust display time in the Switchbot app. The displayed time is
  32. calculated as the internal device time (usually comes from the factory
  33. settings or set by the Switchbot app upon syncing) + offset. The offset
  34. is provided in seconds and can be positive or negative.
  35. Args:
  36. offset_seconds (int): 2^24 maximum, can be negative.
  37. """
  38. abs_offset = abs(offset_seconds)
  39. if abs_offset > MAX_TIME_OFFSET:
  40. raise SwitchbotOperationError(
  41. f"{self.name}: Requested to set_time_offset of {offset_seconds} seconds, allowed +-{MAX_TIME_OFFSET} max."
  42. )
  43. sign_byte = "80" if offset_seconds < 0 else "00"
  44. # Example: 57-0f-68-05-06-80-00-10-00 -> subtract 4096 seconds.
  45. payload = f"{COMMAND_SET_TIME_OFFSET}{sign_byte}{abs_offset:06x}"
  46. result = await self._send_command(payload)
  47. self._validate_result("set_time_offset", result)
  48. async def get_datetime(self) -> dict[str, Any]:
  49. """
  50. Get the current device time and settings as it is displayed. Contains
  51. a time offset, if any was applied (see set_time_offset).
  52. Doesn't include the current time zone.
  53. Returns:
  54. dict: Dictionary containing:
  55. - 12h_mode (bool): True if 12h mode, False if 24h mode.
  56. - year (int)
  57. - month (int)
  58. - day (int)
  59. - hour (int)
  60. - minute (int)
  61. - second (int)
  62. """
  63. # Response Format: 13 bytes, where
  64. # - byte 0: "01" (success)
  65. # - bytes 1-4: temperature, ignored here.
  66. # - byte 5: time display format:
  67. # - "80" - 12h (am/pm)
  68. # - "00" - 24h
  69. # - bytes 6-12: yyyy-MM-dd-hh-mm-ss
  70. # Example: 01-e4-02-94-23-00-07-e9-0c-1e-08-37-01 contains
  71. # "year 2025, 30 December, 08:55:01, displayed in 24h format".
  72. result = await self._send_command(COMMAND_GET_DEVICE_DATETIME)
  73. result = self._validate_result("get_datetime", result, min_length=13)
  74. return {
  75. # Whether the time is displayed in 12h(am/pm) or 24h mode.
  76. "12h_mode": bool(result[5] & 0b10000000),
  77. "year": (result[6] << 8) + result[7],
  78. "month": result[8],
  79. "day": result[9],
  80. "hour": result[10],
  81. "minute": result[11],
  82. "second": result[12],
  83. }
  84. async def set_datetime(
  85. self, timestamp: int, utc_offset_hours: int = 0, utc_offset_minutes: int = 0
  86. ) -> None:
  87. """
  88. Set the device internal time and timezone. Similar to how the
  89. Switchbot app does it upon syncing with the device.
  90. Pay attention to calculating UTC offset hours and minutes, see
  91. examples below.
  92. Args:
  93. timestamp (int): Unix timestamp in seconds.
  94. utc_offset_hours (int): UTC offset in hours, floor()'ed,
  95. within [-12; 14] range.
  96. Examples: -5 for UTC-05:00, -6 for UTC-05:30,
  97. 5 for UTC+05:00, 5 for UTC+5:30.
  98. utc_offset_minutes (int): UTC offset minutes component, always
  99. positive, complements utc_offset_hours.
  100. Examples: 45 for UTC+05:45, 15 for UTC-5:45.
  101. """
  102. if not (-12 <= utc_offset_hours <= 14):
  103. raise SwitchbotOperationError(
  104. f"{self.name}: utc_offset_hours must be between -12 and +14 inclusive, got {utc_offset_hours}"
  105. )
  106. if not (0 <= utc_offset_minutes < 60):
  107. raise SwitchbotOperationError(
  108. f"{self.name}: utc_offset_minutes must be between 0 and 59 inclusive, got {utc_offset_minutes}"
  109. )
  110. # The device doesn't automatically add offset minutes, it expects them
  111. # to come as a part of the timestamp.
  112. adjusted_timestamp = timestamp + utc_offset_minutes * 60
  113. # The timezone is encoded as 1 byte, where 00 stands for UTC-12.
  114. # TZ with minute offset gets floor()ed: 4:30 yields 4, -4:30 yields -5.
  115. utc_byte = utc_offset_hours + 12
  116. payload = (
  117. f"{COMMAND_SET_DEVICE_DATETIME}{utc_byte:02x}"
  118. f"{adjusted_timestamp:016x}{utc_offset_minutes:02x}"
  119. )
  120. result = await self._send_command(payload)
  121. self._validate_result("set_datetime", result)
  122. async def set_time_display_format(self, is_12h_mode: bool = False) -> None:
  123. """
  124. Set the time display format on the device: 12h(AM/PM) or 24h.
  125. Args:
  126. is_12h_mode (bool): True for 12h (AM/PM) mode, False for 24h mode.
  127. """
  128. mode_byte = "80" if is_12h_mode else "00"
  129. payload = f"{COMMAND_SET_DISPLAY_FORMAT}{mode_byte}"
  130. result = await self._send_command(payload)
  131. self._validate_result("set_time_display_format", result)
  132. def _validate_result(
  133. self, op_name: str, result: bytes | None, min_length: int | None = None
  134. ) -> bytes:
  135. if not self._check_command_result(result, 0, {1}):
  136. raise SwitchbotOperationError(
  137. f"{self.name}: Unexpected response code for {op_name} (result={result.hex() if result else 'None'} rssi={self.rssi})"
  138. )
  139. assert result is not None
  140. if min_length is not None and len(result) < min_length:
  141. raise SwitchbotOperationError(
  142. f"{self.name}: Unexpected response len for {op_name}, wanted at least {min_length} (result={result.hex() if result else 'None'} rssi={self.rssi})"
  143. )
  144. return result