relay_switch.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import logging
  2. import time
  3. from typing import Any
  4. from bleak.backends.device import BLEDevice
  5. from ..const import SwitchbotModel
  6. from .device import (
  7. SwitchbotEncryptedDevice,
  8. SwitchbotSequenceDevice,
  9. update_after_operation,
  10. )
  11. _LOGGER = logging.getLogger(__name__)
  12. COMMAND_HEADER = "57"
  13. COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f70010000"
  14. COMMAND_TURN_ON = f"{COMMAND_HEADER}0f70010100"
  15. COMMAND_TOGGLE = f"{COMMAND_HEADER}0f70010200"
  16. COMMAND_GET_VOLTAGE_AND_CURRENT = f"{COMMAND_HEADER}0f7106000000"
  17. COMMAND_GET_BASIC_INFO = f"{COMMAND_HEADER}0f7181"
  18. COMMAND_GET_CHANNEL1_INFO = f"{COMMAND_HEADER}0f710600{{}}{{}}"
  19. COMMAND_GET_CHANNEL2_INFO = f"{COMMAND_HEADER}0f710601{{}}{{}}"
  20. MULTI_CHANNEL_COMMANDS_TURN_ON = {
  21. SwitchbotModel.RELAY_SWITCH_2PM: {
  22. 1: "570f70010d00",
  23. 2: "570f70010700",
  24. }
  25. }
  26. MULTI_CHANNEL_COMMANDS_TURN_OFF = {
  27. SwitchbotModel.RELAY_SWITCH_2PM: {
  28. 1: "570f70010c00",
  29. 2: "570f70010300",
  30. }
  31. }
  32. MULTI_CHANNEL_COMMANDS_TOGGLE = {
  33. SwitchbotModel.RELAY_SWITCH_2PM: {
  34. 1: "570f70010e00",
  35. 2: "570f70010b00",
  36. }
  37. }
  38. MULTI_CHANNEL_COMMANDS_GET_VOLTAGE_AND_CURRENT = {
  39. SwitchbotModel.RELAY_SWITCH_2PM: {
  40. 1: COMMAND_GET_CHANNEL1_INFO,
  41. 2: COMMAND_GET_CHANNEL2_INFO,
  42. }
  43. }
  44. class SwitchbotRelaySwitch(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
  45. """Representation of a Switchbot relay switch 1pm."""
  46. def __init__(
  47. self,
  48. device: BLEDevice,
  49. key_id: str,
  50. encryption_key: str,
  51. interface: int = 0,
  52. model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_1PM,
  53. **kwargs: Any,
  54. ) -> None:
  55. super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
  56. @classmethod
  57. async def verify_encryption_key(
  58. cls,
  59. device: BLEDevice,
  60. key_id: str,
  61. encryption_key: str,
  62. model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_1PM,
  63. **kwargs: Any,
  64. ) -> bool:
  65. return await super().verify_encryption_key(
  66. device, key_id, encryption_key, model, **kwargs
  67. )
  68. def get_current_time_and_start_time(self) -> int:
  69. """Get current time in seconds since epoch."""
  70. current_time = int(time.time())
  71. current_time_hex = f"{current_time:08x}"
  72. current_day_start_time = int(current_time / 86400) * 86400
  73. current_day_start_time_hex = f"{current_day_start_time:08x}"
  74. return current_time_hex, current_day_start_time_hex
  75. async def get_basic_info(self) -> dict[str, Any] | None:
  76. """Get device basic settings."""
  77. current_time_hex, current_day_start_time_hex = self.get_current_time_and_start_time()
  78. if not (_data := await self._get_basic_info(COMMAND_GET_BASIC_INFO)):
  79. return None
  80. if not (_channel1_data := await self._get_basic_info(COMMAND_GET_CHANNEL1_INFO.format(current_time_hex, current_day_start_time_hex))):
  81. return None
  82. common_data = {
  83. "isOn": bool(_data[2] & 0b10000000),
  84. "firmware": _data[16] / 10.0,
  85. "use_time": int.from_bytes(_channel1_data[7:9], "big"),
  86. }
  87. user_data = {
  88. "Electricity Usage Today": int.from_bytes(_channel1_data[1:4], "big"),
  89. "Electricity Usage Yesterday": int.from_bytes(_channel1_data[4:7], "big"),
  90. "voltage": int.from_bytes(_channel1_data[9:11], "big") / 10.0,
  91. "current": int.from_bytes(_channel1_data[11:13], "big"),
  92. "power": int.from_bytes(_channel1_data[13:15], "big") / 10.0,
  93. }
  94. garage_door_opener_data = {
  95. "door_open": not bool(_data[7] & 0b00100000),
  96. }
  97. _LOGGER.debug("common_data: %s, garage_door_opener_data: %s", common_data, garage_door_opener_data)
  98. if self._model == SwitchbotModel.RELAY_SWITCH_1:
  99. return common_data
  100. if self._model == SwitchbotModel.GARAGE_DOOR_OPENER:
  101. return common_data | garage_door_opener_data
  102. return common_data | user_data
  103. @update_after_operation
  104. async def turn_on(self) -> bool:
  105. """Turn device on."""
  106. result = await self._send_command(COMMAND_TURN_ON)
  107. return self._check_command_result(result, 0, {1})
  108. @update_after_operation
  109. async def turn_off(self) -> bool:
  110. """Turn device off."""
  111. result = await self._send_command(COMMAND_TURN_OFF)
  112. return self._check_command_result(result, 0, {1})
  113. @update_after_operation
  114. async def async_toggle(self, **kwargs) -> bool:
  115. """Toggle device."""
  116. result = await self._send_command(COMMAND_TOGGLE)
  117. return self._check_command_result(result, 0, {1})
  118. def is_on(self) -> bool | None:
  119. """Return switch state from cache."""
  120. return self._get_adv_value("isOn")
  121. class SwitchbotRelaySwitch2PM(SwitchbotRelaySwitch):
  122. """Representation of a Switchbot relay switch 2pm."""
  123. def __init__(
  124. self,
  125. device: BLEDevice,
  126. key_id: str,
  127. encryption_key: str,
  128. interface: int = 0,
  129. model: SwitchbotModel = SwitchbotModel.RELAY_SWITCH_2PM,
  130. **kwargs: Any,
  131. ) -> None:
  132. super().__init__(device, key_id, encryption_key, interface, model, **kwargs)
  133. self._channel = 2
  134. @property
  135. def channel(self) -> int:
  136. return self._channel
  137. def get_parsed_data(self, channel: int | None = None) -> dict[str, Any]:
  138. """Return parsed device data, optionally for a specific channel."""
  139. data = self.data.get("data") or {}
  140. return data.get(channel, {})
  141. async def get_basic_info(self):
  142. current_time_hex, current_day_start_time_hex = self.get_current_time_and_start_time()
  143. if not (common_data := await super().get_basic_info()):
  144. return None
  145. if not (
  146. _channel2_data := await self._get_basic_info(COMMAND_GET_CHANNEL2_INFO.format(current_time_hex, current_day_start_time_hex))
  147. ):
  148. return None
  149. result = {
  150. 1: common_data,
  151. 2: {
  152. "isOn": bool(_channel2_data[2] & 0b01000000),
  153. "Electricity Usage Today": int.from_bytes(_channel2_data[1:4], "big"),
  154. "Electricity Usage Yesterday": int.from_bytes(_channel2_data[4:7], "big"),
  155. "use_time": int.from_bytes(_channel2_data[7:9], "big"),
  156. "voltage": int.from_bytes(_channel2_data[9:11], "big") / 10.0,
  157. "current": int.from_bytes(_channel2_data[11:13], "big"),
  158. "power": int.from_bytes(_channel2_data[13:15], "big") / 10.0,
  159. }
  160. }
  161. _LOGGER.debug("Multi channel basic info: %s", result)
  162. return result
  163. @update_after_operation
  164. async def turn_on(self, channel: int) -> bool:
  165. """Turn device on."""
  166. result = await self._send_command(MULTI_CHANNEL_COMMANDS_TURN_ON[self._model][channel])
  167. return self._check_command_result(result, 0, {1})
  168. @update_after_operation
  169. async def turn_off(self, channel: int) -> bool:
  170. """Turn device off."""
  171. result = await self._send_command(MULTI_CHANNEL_COMMANDS_TURN_OFF[self._model][channel])
  172. return self._check_command_result(result, 0, {1})
  173. @update_after_operation
  174. async def async_toggle(self, channel: int) -> bool:
  175. """Toggle device."""
  176. result = await self._send_command(MULTI_CHANNEL_COMMANDS_TOGGLE[self._model][channel])
  177. return self._check_command_result(result, 0, {1})
  178. def is_on(self, channel: int) -> bool | None:
  179. """Return switch state from cache."""
  180. return self._get_adv_value("isOn", channel)
  181. def switch_mode(self, channel: int) -> bool | None:
  182. """Return true or false from cache."""
  183. return self._get_adv_value("switchMode", channel)