1
0

test_meter_pro.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. from unittest.mock import AsyncMock
  2. import pytest
  3. from bleak.backends.device import BLEDevice
  4. from switchbot import SwitchbotOperationError
  5. from switchbot.devices.meter_pro import (
  6. MAX_TIME_OFFSET,
  7. SwitchbotMeterPro,
  8. SwitchbotMeterProCO2,
  9. )
  10. def create_device(cls=SwitchbotMeterProCO2):
  11. ble_device = BLEDevice(
  12. address="aa:bb:cc:dd:ee:ff", name="any", details={"rssi": -80}
  13. )
  14. device = cls(ble_device)
  15. device._send_command = AsyncMock()
  16. return device
  17. @pytest.mark.asyncio
  18. @pytest.mark.parametrize(
  19. (
  20. "device_response",
  21. "expected_offset",
  22. ),
  23. [
  24. ("0100101bc9", 1055689), # 01 (success) 00 (plus offset) 10 1b c9 (1055689)
  25. ("0180101bc9", -1055689), # 01 (success) 80 (minus offset) 10 1b c9 (1055689)
  26. ],
  27. )
  28. async def test_get_time_offset(device_response: str, expected_offset: int):
  29. device = create_device()
  30. device._send_command.return_value = bytes.fromhex(device_response)
  31. offset = await device.get_time_offset()
  32. device._send_command.assert_called_with("570f690506")
  33. assert offset == expected_offset
  34. @pytest.mark.asyncio
  35. async def test_get_time_offset_failure():
  36. device = create_device()
  37. # Invalid 1st byte
  38. device._send_command.return_value = bytes.fromhex("0080101bc9")
  39. with pytest.raises(SwitchbotOperationError):
  40. await device.get_time_offset()
  41. device._send_command.assert_called_with("570f690506")
  42. @pytest.mark.asyncio
  43. async def test_get_time_offset_wrong_response():
  44. device = create_device()
  45. # Response too short (only status byte returned)
  46. device._send_command.return_value = bytes.fromhex("01")
  47. with pytest.raises(SwitchbotOperationError):
  48. await device.get_time_offset()
  49. @pytest.mark.asyncio
  50. @pytest.mark.parametrize(
  51. (
  52. "offset_sec",
  53. "expected_payload",
  54. ),
  55. [
  56. (1055689, "00101bc9"), # "00" for positive offset, 101bc9 for 1055689
  57. (-4096, "80001000"), # "80" for negative offset, 001000 for 4096
  58. (0, "00000000"),
  59. (-0, "00000000"), # -0 == 0 in Python
  60. ],
  61. )
  62. async def test_set_time_offset(offset_sec: int, expected_payload: str):
  63. device = create_device()
  64. device._send_command.return_value = bytes.fromhex("01")
  65. await device.set_time_offset(offset_sec)
  66. device._send_command.assert_called_with("570f680506" + expected_payload)
  67. @pytest.mark.asyncio
  68. async def test_set_time_offset_too_large():
  69. device = create_device()
  70. with pytest.raises(SwitchbotOperationError):
  71. await device.set_time_offset(MAX_TIME_OFFSET + 1)
  72. with pytest.raises(SwitchbotOperationError):
  73. await device.set_time_offset(-(MAX_TIME_OFFSET + 1))
  74. @pytest.mark.asyncio
  75. async def test_set_time_offset_failure():
  76. device = create_device()
  77. device._send_command.return_value = bytes.fromhex("00")
  78. with pytest.raises(SwitchbotOperationError):
  79. await device.set_time_offset(100)
  80. @pytest.mark.asyncio
  81. async def test_get_datetime_success():
  82. device = create_device()
  83. # Mock response:
  84. # byte 0: 01 (success)
  85. # bytes 1-4: e4 02 94 23 (temp, ignored)
  86. # byte 5: 00 (24h mode)
  87. # bytes 6-7: 07 e9 (year 2025)
  88. # byte 8: 0c (Dec)
  89. # byte 9: 1e (30)
  90. # byte 10: 08 (Hour)
  91. # byte 11: 37 (Minute = 55)
  92. # byte 12: 01 (Second)
  93. response_hex = "01e40294230007e90c1e083701"
  94. device._send_command.return_value = bytes.fromhex(response_hex)
  95. result = await device.get_datetime()
  96. device._send_command.assert_called_with("570f6901")
  97. assert result["12h_mode"] is False
  98. assert result["year"] == 2025
  99. assert result["month"] == 12
  100. assert result["day"] == 30
  101. assert result["hour"] == 8
  102. assert result["minute"] == 55
  103. assert result["second"] == 1
  104. @pytest.mark.asyncio
  105. async def test_get_datetime_12h_mode():
  106. device = create_device()
  107. # byte 5: 80 (12h mode)
  108. # Time: 12:00:00
  109. response_hex = "010000000080000001010c0000"
  110. device._send_command.return_value = bytes.fromhex(response_hex)
  111. result = await device.get_datetime()
  112. device._send_command.assert_called_with("570f6901")
  113. assert result["12h_mode"] is True
  114. assert result["year"] == 0
  115. assert result["month"] == 1
  116. assert result["day"] == 1
  117. assert result["hour"] == 12
  118. assert result["minute"] == 0
  119. assert result["second"] == 0
  120. @pytest.mark.asyncio
  121. async def test_get_datetime_failure():
  122. device = create_device()
  123. device._send_command.return_value = bytes.fromhex("00")
  124. with pytest.raises(SwitchbotOperationError):
  125. await device.get_datetime()
  126. @pytest.mark.asyncio
  127. async def test_get_datetime_wrong_response():
  128. device = create_device()
  129. device._send_command.return_value = bytes.fromhex("0100")
  130. with pytest.raises(SwitchbotOperationError):
  131. await device.get_datetime()
  132. @pytest.mark.asyncio
  133. @pytest.mark.parametrize(
  134. (
  135. "timestamp",
  136. "utc_offset_hours",
  137. "utc_offset_minutes",
  138. "expected_ts",
  139. "expected_utc",
  140. "expected_min",
  141. ),
  142. [
  143. (1709251200, 0, 0, "65e11a80", "0c", "00"), # 2024-03-01T00:00:00+00:00
  144. (1709251200, 1, 0, "65e11a80", "0d", "00"), # 2024-03-01T00:00:00+01:00
  145. (1709251200, 5, 45, "65e1250c", "11", "2d"), # 2024-03-01T00:00:00+05:45
  146. (1709251200, -6, 15, "65e11e04", "06", "0f"), # 2024-03-01T00:00:00-05:45
  147. ],
  148. )
  149. async def test_set_datetime( # noqa: PLR0913
  150. *,
  151. timestamp: int,
  152. utc_offset_hours: int,
  153. utc_offset_minutes: int,
  154. expected_ts: str,
  155. expected_utc: str,
  156. expected_min: str,
  157. ):
  158. device = create_device()
  159. device._send_command.return_value = bytes.fromhex("01")
  160. await device.set_datetime(
  161. timestamp,
  162. utc_offset_hours=utc_offset_hours,
  163. utc_offset_minutes=utc_offset_minutes,
  164. )
  165. expected_ts = expected_ts.zfill(16)
  166. expected_payload = "57000503" + expected_utc + expected_ts + expected_min
  167. device._send_command.assert_called_with(expected_payload)
  168. @pytest.mark.asyncio
  169. @pytest.mark.parametrize(
  170. "bad_hour",
  171. [-13, 15],
  172. )
  173. async def test_set_datetime_invalid_utc_offset_hours(bad_hour: int):
  174. device = create_device()
  175. with pytest.raises(SwitchbotOperationError):
  176. await device.set_datetime(1709251200, utc_offset_hours=bad_hour)
  177. @pytest.mark.asyncio
  178. @pytest.mark.parametrize(
  179. "bad_min",
  180. [-1, 60],
  181. )
  182. async def test_set_datetime_invalid_utc_offset_minutes(bad_min: int):
  183. device = create_device()
  184. with pytest.raises(SwitchbotOperationError):
  185. await device.set_datetime(1709251200, utc_offset_minutes=bad_min)
  186. @pytest.mark.asyncio
  187. @pytest.mark.parametrize(
  188. ("is_12h_mode", "expected_payload"),
  189. [
  190. (True, "80"),
  191. (False, "00"),
  192. ],
  193. )
  194. async def test_set_time_display_format(is_12h_mode: bool, expected_payload: str):
  195. device = create_device()
  196. device._send_command.return_value = bytes.fromhex("01")
  197. await device.set_time_display_format(is_12h_mode=is_12h_mode)
  198. device._send_command.assert_called_with("570f680505" + expected_payload)
  199. @pytest.mark.asyncio
  200. async def test_set_time_display_format_failure():
  201. device = create_device()
  202. device._send_command.return_value = bytes.fromhex("00")
  203. with pytest.raises(SwitchbotOperationError):
  204. await device.set_time_display_format(is_12h_mode=True)
  205. @pytest.mark.asyncio
  206. async def test_meter_pro_shares_datetime_protocol_with_co2_variant():
  207. """The plain Meter Pro (no CO2 sensor) uses the same BLE commands as the CO2 variant."""
  208. device = create_device(SwitchbotMeterPro)
  209. device._send_command.return_value = bytes.fromhex("01e40294230007e90c1e083701")
  210. result = await device.get_datetime()
  211. device._send_command.assert_called_with("570f6901")
  212. assert result["year"] == 2025
  213. device._send_command.return_value = bytes.fromhex("01")
  214. await device.set_datetime(1709251200)
  215. device._send_command.assert_called_with(
  216. "57000503" + "0c" + "65e11a80".zfill(16) + "00"
  217. )