evaporative_humidifier.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import logging
  2. from typing import Any
  3. from bleak.backends.device import BLEDevice
  4. from ..adv_parsers.humidifier import calculate_temperature_and_humidity
  5. from ..const import SwitchbotModel
  6. from ..const.evaporative_humidifier import (
  7. TARGET_HUMIDITY_MODES,
  8. HumidifierAction,
  9. HumidifierMode,
  10. HumidifierWaterLevel,
  11. )
  12. from .device import (
  13. SwitchbotEncryptedDevice,
  14. SwitchbotOperationError,
  15. SwitchbotSequenceDevice,
  16. update_after_operation,
  17. )
  18. _LOGGER = logging.getLogger(__name__)
  19. COMMAND_HEADER = "57"
  20. COMMAND_GET_CK_IV = f"{COMMAND_HEADER}0f2103"
  21. COMMAND_TURN_ON = f"{COMMAND_HEADER}0f430101"
  22. COMMAND_TURN_OFF = f"{COMMAND_HEADER}0f430100"
  23. COMMAND_CHILD_LOCK_ON = f"{COMMAND_HEADER}0f430501"
  24. COMMAND_CHILD_LOCK_OFF = f"{COMMAND_HEADER}0f430500"
  25. COMMAND_AUTO_DRY_ON = f"{COMMAND_HEADER}0f430a01"
  26. COMMAND_AUTO_DRY_OFF = f"{COMMAND_HEADER}0f430a02"
  27. COMMAND_SET_MODE = f"{COMMAND_HEADER}0f4302"
  28. COMMAND_GET_BASIC_INFO = f"{COMMAND_HEADER}000300"
  29. COMMAND_SET_DRYING_FILTER = f"{COMMAND_TURN_ON}08"
  30. MODES_COMMANDS = {
  31. HumidifierMode.HIGH: "010100",
  32. HumidifierMode.MEDIUM: "010200",
  33. HumidifierMode.LOW: "010300",
  34. HumidifierMode.QUIET: "010400",
  35. HumidifierMode.TARGET_HUMIDITY: "0200",
  36. HumidifierMode.SLEEP: "0300",
  37. HumidifierMode.AUTO: "040000",
  38. }
  39. DEVICE_GET_BASIC_SETTINGS_KEY = "570f4481"
  40. class SwitchbotEvaporativeHumidifier(SwitchbotSequenceDevice, SwitchbotEncryptedDevice):
  41. """Representation of a Switchbot Evaporative Humidifier"""
  42. def __init__(
  43. self,
  44. device: BLEDevice,
  45. key_id: str,
  46. encryption_key: str,
  47. interface: int = 0,
  48. model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  49. **kwargs: Any,
  50. ) -> None:
  51. self._force_next_update = False
  52. super().__init__(device, key_id, encryption_key, model, interface, **kwargs)
  53. @classmethod
  54. async def verify_encryption_key(
  55. cls,
  56. device: BLEDevice,
  57. key_id: str,
  58. encryption_key: str,
  59. model: SwitchbotModel = SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  60. **kwargs: Any,
  61. ) -> bool:
  62. return await super().verify_encryption_key(
  63. device, key_id, encryption_key, model, **kwargs
  64. )
  65. async def get_basic_info(self) -> dict[str, Any] | None:
  66. """Get device basic settings."""
  67. if not (_data := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
  68. return None
  69. _LOGGER.debug("basic info data: %s", _data.hex())
  70. isOn = bool(_data[1] & 0b10000000)
  71. mode = HumidifierMode(_data[1] & 0b00001111)
  72. over_humidify_protection = bool(_data[2] & 0b10000000)
  73. child_lock = bool(_data[2] & 0b00100000)
  74. tank_removed = bool(_data[2] & 0b00000100)
  75. tilted_alert = bool(_data[2] & 0b00000010)
  76. filter_missing = bool(_data[2] & 0b00000001)
  77. is_meter_binded = bool(_data[3] & 0b10000000)
  78. _temp_c, _temp_f, humidity = calculate_temperature_and_humidity(
  79. _data[3:6], is_meter_binded
  80. )
  81. water_level = HumidifierWaterLevel(_data[5] & 0b00000011).name.lower()
  82. filter_run_time = int.from_bytes(_data[6:8], byteorder="big") & 0xFFF
  83. target_humidity = _data[10] & 0b01111111
  84. return {
  85. "isOn": isOn,
  86. "mode": mode,
  87. "over_humidify_protection": over_humidify_protection,
  88. "child_lock": child_lock,
  89. "tank_removed": tank_removed,
  90. "tilted_alert": tilted_alert,
  91. "filter_missing": filter_missing,
  92. "is_meter_binded": is_meter_binded,
  93. "humidity": humidity,
  94. "temperature": _temp_c,
  95. "temp": {"c": _temp_c, "f": _temp_f},
  96. "water_level": water_level,
  97. "filter_run_time": filter_run_time,
  98. "target_humidity": target_humidity,
  99. }
  100. @update_after_operation
  101. async def turn_on(self) -> bool:
  102. """Turn device on."""
  103. result = await self._send_command(COMMAND_TURN_ON)
  104. return self._check_command_result(result, 0, {1})
  105. @update_after_operation
  106. async def turn_off(self) -> bool:
  107. """Turn device off."""
  108. result = await self._send_command(COMMAND_TURN_OFF)
  109. return self._check_command_result(result, 0, {1})
  110. @update_after_operation
  111. async def set_target_humidity(self, target_humidity: int) -> bool:
  112. """Set target humidity."""
  113. self._validate_water_level()
  114. self._validate_mode_for_target_humidity()
  115. command = (
  116. COMMAND_SET_MODE
  117. + MODES_COMMANDS[self.get_mode()]
  118. + f"{target_humidity:02x}"
  119. )
  120. result = await self._send_command(command)
  121. return self._check_command_result(result, 0, {1})
  122. @update_after_operation
  123. async def set_mode(self, mode: HumidifierMode) -> bool:
  124. """Set device mode."""
  125. self._validate_water_level()
  126. self._validate_meter_binding(mode)
  127. if mode == HumidifierMode.DRYING_FILTER:
  128. command = COMMAND_SET_DRYING_FILTER
  129. else:
  130. command = COMMAND_SET_MODE + MODES_COMMANDS[mode]
  131. if mode in TARGET_HUMIDITY_MODES:
  132. target_humidity = self.get_target_humidity()
  133. if target_humidity is None:
  134. raise SwitchbotOperationError(
  135. "Target humidity must be set before switching to target humidity mode or sleep mode"
  136. )
  137. command += f"{target_humidity:02x}"
  138. result = await self._send_command(command)
  139. return self._check_command_result(result, 0, {1})
  140. def _validate_water_level(self) -> None:
  141. """Validate that the water level is not empty."""
  142. if self.get_water_level() == HumidifierWaterLevel.EMPTY.name.lower():
  143. raise SwitchbotOperationError(
  144. "Cannot perform operation when water tank is empty"
  145. )
  146. def _validate_mode_for_target_humidity(self) -> None:
  147. """Validate that the current mode supports target humidity."""
  148. if self.get_mode() not in TARGET_HUMIDITY_MODES:
  149. raise SwitchbotOperationError(
  150. "Target humidity can only be set in target humidity mode or sleep mode"
  151. )
  152. def _validate_meter_binding(self, mode: HumidifierMode) -> None:
  153. """Validate that the meter is binded for specific modes."""
  154. if not self.is_meter_binded() and mode in [
  155. HumidifierMode.TARGET_HUMIDITY,
  156. HumidifierMode.AUTO,
  157. ]:
  158. raise SwitchbotOperationError(
  159. "Cannot set target humidity or auto mode when meter is not binded"
  160. )
  161. @update_after_operation
  162. async def set_child_lock(self, enabled: bool) -> bool:
  163. """Set child lock."""
  164. result = await self._send_command(
  165. COMMAND_CHILD_LOCK_ON if enabled else COMMAND_CHILD_LOCK_OFF
  166. )
  167. return self._check_command_result(result, 0, {1})
  168. def is_on(self) -> bool | None:
  169. """Return state from cache."""
  170. return self._get_adv_value("isOn")
  171. def get_mode(self) -> HumidifierMode | None:
  172. """Return state from cache."""
  173. return self._get_adv_value("mode")
  174. def is_child_lock_enabled(self) -> bool | None:
  175. """Return state from cache."""
  176. return self._get_adv_value("child_lock")
  177. def is_over_humidify_protection_enabled(self) -> bool | None:
  178. """Return state from cache."""
  179. return self._get_adv_value("over_humidify_protection")
  180. def is_tank_removed(self) -> bool | None:
  181. """Return state from cache."""
  182. return self._get_adv_value("tank_removed")
  183. def is_filter_missing(self) -> bool | None:
  184. """Return state from cache."""
  185. return self._get_adv_value("filter_missing")
  186. def is_filter_alert_on(self) -> bool | None:
  187. """Return state from cache."""
  188. return self._get_adv_value("filter_alert")
  189. def is_tilted_alert_on(self) -> bool | None:
  190. """Return state from cache."""
  191. return self._get_adv_value("tilted_alert")
  192. def get_water_level(self) -> HumidifierWaterLevel | None:
  193. """Return state from cache."""
  194. return self._get_adv_value("water_level")
  195. def get_filter_run_time(self) -> int | None:
  196. """Return state from cache."""
  197. return self._get_adv_value("filter_run_time")
  198. def get_target_humidity(self) -> int | None:
  199. """Return state from cache."""
  200. return self._get_adv_value("target_humidity")
  201. def get_humidity(self) -> int | None:
  202. """Return state from cache."""
  203. return self._get_adv_value("humidity")
  204. def get_temperature(self) -> float | None:
  205. """Return state from cache."""
  206. return self._get_adv_value("temperature")
  207. def get_action(self) -> int:
  208. """Return current action from cache."""
  209. if not self.is_on():
  210. return HumidifierAction.OFF
  211. if self.get_mode() != HumidifierMode.DRYING_FILTER:
  212. return HumidifierAction.HUMIDIFYING
  213. return HumidifierAction.DRYING
  214. def is_meter_binded(self) -> bool | None:
  215. """Return meter bind state from cache."""
  216. return self._get_adv_value("is_meter_binded")