__init__.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. """Library to handle connection with Switchbot."""
  2. from __future__ import annotations
  3. import binascii
  4. import logging
  5. from threading import Lock
  6. import time
  7. from typing import Any
  8. import bluepy
  9. DEFAULT_RETRY_COUNT = 3
  10. DEFAULT_RETRY_TIMEOUT = 1
  11. DEFAULT_SCAN_TIMEOUT = 5
  12. # Keys common to all device types
  13. DEVICE_GET_BASIC_SETTINGS_KEY = "5702"
  14. DEVICE_SET_MODE_KEY = "5703"
  15. DEVICE_SET_EXTENDED_KEY = "570f"
  16. # Bot keys
  17. PRESS_KEY = "570100"
  18. ON_KEY = "570101"
  19. OFF_KEY = "570102"
  20. DOWN_KEY = "570103"
  21. UP_KEY = "570104"
  22. # Curtain keys
  23. OPEN_KEY = "570f450105ff00" # 570F4501010100
  24. CLOSE_KEY = "570f450105ff64" # 570F4501010164
  25. POSITION_KEY = "570F450105ff" # +actual_position ex: 570F450105ff32 for 50%
  26. STOP_KEY = "570F450100ff"
  27. CURTAIN_EXT_SUM_KEY = "570f460401"
  28. CURTAIN_EXT_ADV_KEY = "570f460402"
  29. CURTAIN_EXT_CHAIN_INFO_KEY = "570f468101"
  30. # Base key when encryption is set
  31. KEY_PASSWORD_PREFIX = "571"
  32. _LOGGER = logging.getLogger(__name__)
  33. CONNECT_LOCK = Lock()
  34. def _sb_uuid(comms_type: str = "service") -> bluepy.btle.UUID:
  35. """Return Switchbot UUID."""
  36. _uuid = {"tx": "002", "rx": "003", "service": "d00"}
  37. if comms_type in _uuid:
  38. return bluepy.btle.UUID(f"cba20{_uuid[comms_type]}-224d-11e6-9fb8-0002a5d5c51b")
  39. return "Incorrect type, choose between: tx, rx or service"
  40. def _process_wohand(data: bytes) -> dict[str, bool | int]:
  41. """Process woHand/Bot services data."""
  42. _switch_mode = bool(data[1] & 0b10000000)
  43. _bot_data = {
  44. "switchMode": _switch_mode,
  45. "isOn": not bool(data[1] & 0b01000000) if _switch_mode else False,
  46. "battery": data[2] & 0b01111111,
  47. }
  48. return _bot_data
  49. def _process_wocurtain(data: bytes, reverse: bool = True) -> dict[str, bool | int]:
  50. """Process woCurtain/Curtain services data."""
  51. _position = max(min(data[3] & 0b01111111, 100), 0)
  52. _curtain_data = {
  53. "calibration": bool(data[1] & 0b01000000),
  54. "battery": data[2] & 0b01111111,
  55. "inMotion": bool(data[3] & 0b10000000),
  56. "position": (100 - _position) if reverse else _position,
  57. "lightLevel": (data[4] >> 4) & 0b00001111,
  58. "deviceChain": data[4] & 0b00000111,
  59. }
  60. return _curtain_data
  61. def _process_wosensorth(data: bytes) -> dict[str, object]:
  62. """Process woSensorTH/Temp sensor services data."""
  63. _temp_sign = 1 if data[4] & 0b10000000 else -1
  64. _temp_c = _temp_sign * ((data[4] & 0b01111111) + (data[3] / 10))
  65. _temp_f = (_temp_c * 9 / 5) + 32
  66. _temp_f = (_temp_f * 10) / 10
  67. _wosensorth_data = {
  68. "temp": {"c": _temp_c, "f": _temp_f},
  69. "fahrenheit": bool(data[5] & 0b10000000),
  70. "humidity": data[5] & 0b01111111,
  71. "battery": data[2] & 0b01111111,
  72. }
  73. return _wosensorth_data
  74. def _process_btle_adv_data(dev: bluepy.btle.ScanEntry) -> dict[str, Any]:
  75. """Process bt le adv data."""
  76. _adv_data = {"mac_address": dev.addr}
  77. _data = dev.getValue(22)[2:]
  78. supported_types: dict[str, dict[str, Any]] = {
  79. "H": {"modelName": "WoHand", "func": _process_wohand},
  80. "c": {"modelName": "WoCurtain", "func": _process_wocurtain},
  81. "T": {"modelName": "WoSensorTH", "func": _process_wosensorth},
  82. }
  83. _model = chr(_data[0] & 0b01111111)
  84. _adv_data["isEncrypted"] = bool(_data[0] & 0b10000000)
  85. _adv_data["model"] = _model
  86. if _model in supported_types:
  87. _adv_data["data"] = supported_types[_model]["func"](_data)
  88. _adv_data["data"]["rssi"] = dev.rssi
  89. _adv_data["modelName"] = supported_types[_model]["modelName"]
  90. else:
  91. _adv_data["rawAdvData"] = dev.getValueText(22)
  92. return _adv_data
  93. class GetSwitchbotDevices:
  94. """Scan for all Switchbot devices and return by type."""
  95. def __init__(self, interface: int = 0) -> None:
  96. """Get switchbot devices class constructor."""
  97. self._interface = interface
  98. self._adv_data: dict[str, Any] = {}
  99. def discover(
  100. self,
  101. retry: int = DEFAULT_RETRY_COUNT,
  102. scan_timeout: int = DEFAULT_SCAN_TIMEOUT,
  103. passive: bool = False,
  104. mac: str | None = None,
  105. ) -> dict[str, Any]:
  106. """Find switchbot devices and their advertisement data."""
  107. devices = None
  108. with CONNECT_LOCK:
  109. try:
  110. devices = bluepy.btle.Scanner(self._interface).scan(
  111. scan_timeout, passive
  112. )
  113. except bluepy.btle.BTLEManagementError:
  114. _LOGGER.error("Error scanning for switchbot devices", exc_info=True)
  115. if devices is None:
  116. if retry < 1:
  117. _LOGGER.error(
  118. "Scanning for Switchbot devices failed. Stop trying", exc_info=True
  119. )
  120. return self._adv_data
  121. _LOGGER.warning(
  122. "Error scanning for Switchbot devices. Retrying (remaining: %d)",
  123. retry,
  124. )
  125. time.sleep(DEFAULT_RETRY_TIMEOUT)
  126. return self.discover(
  127. retry=retry - 1,
  128. scan_timeout=scan_timeout,
  129. passive=passive,
  130. mac=mac,
  131. )
  132. for dev in devices:
  133. if dev.getValueText(7) == str(_sb_uuid()):
  134. dev_id = dev.addr.replace(":", "")
  135. if mac:
  136. if dev.addr.lower() == mac.lower():
  137. self._adv_data[dev_id] = _process_btle_adv_data(dev)
  138. else:
  139. self._adv_data[dev_id] = _process_btle_adv_data(dev)
  140. return self._adv_data
  141. def get_curtains(self) -> dict:
  142. """Return all WoCurtain/Curtains devices with services data."""
  143. if not self._adv_data:
  144. self.discover()
  145. _curtain_devices = {
  146. device: data
  147. for device, data in self._adv_data.items()
  148. if data.get("model") == "c"
  149. }
  150. return _curtain_devices
  151. def get_bots(self) -> dict:
  152. """Return all WoHand/Bot devices with services data."""
  153. if not self._adv_data:
  154. self.discover()
  155. _bot_devices = {
  156. device: data
  157. for device, data in self._adv_data.items()
  158. if data.get("model") == "H"
  159. }
  160. return _bot_devices
  161. def get_tempsensors(self) -> dict:
  162. """Return all WoSensorTH/Temp sensor devices with services data."""
  163. if not self._adv_data:
  164. self.discover()
  165. _bot_temp = {
  166. device: data
  167. for device, data in self._adv_data.items()
  168. if data.get("model") == "T"
  169. }
  170. return _bot_temp
  171. def get_device_data(self, mac: str) -> dict:
  172. """Return data for specific device."""
  173. if not self._adv_data:
  174. self.discover()
  175. _switchbot_data = {
  176. device: data
  177. for device, data in self._adv_data.items()
  178. if data.get("mac_address") == mac
  179. }
  180. return _switchbot_data
  181. class SwitchbotDevice(bluepy.btle.Peripheral):
  182. """Base Representation of a Switchbot Device."""
  183. def __init__(
  184. self,
  185. mac: str,
  186. password: str | None = None,
  187. interface: int = 0,
  188. **kwargs: Any,
  189. ) -> None:
  190. """Switchbot base class constructor."""
  191. bluepy.btle.Peripheral.__init__(
  192. self,
  193. deviceAddr=None,
  194. addrType=bluepy.btle.ADDR_TYPE_RANDOM,
  195. iface=interface,
  196. )
  197. self._interface = interface
  198. self._mac = mac.replace("-", ":").lower()
  199. self._sb_adv_data: dict[str, Any] = {}
  200. self._scan_timeout: int = kwargs.pop("scan_timeout", DEFAULT_SCAN_TIMEOUT)
  201. self._retry_count: int = kwargs.pop("retry_count", DEFAULT_RETRY_COUNT)
  202. if password is None or password == "":
  203. self._password_encoded = None
  204. else:
  205. self._password_encoded = "%x" % (
  206. binascii.crc32(password.encode("ascii")) & 0xFFFFFFFF
  207. )
  208. # pylint: disable=arguments-differ
  209. def _connect(self, retry: int, timeout: int | None = None) -> None:
  210. _LOGGER.debug("Connecting to Switchbot")
  211. if retry < 1: # failsafe
  212. self._stopHelper()
  213. return
  214. if self._helper is None:
  215. self._startHelper(self._interface)
  216. self._writeCmd("conn %s %s\n" % (self._mac, bluepy.btle.ADDR_TYPE_RANDOM))
  217. rsp = self._getResp(["stat", "err"], timeout)
  218. while rsp.get("state") and rsp["state"][0] in [
  219. "tryconn",
  220. "scan",
  221. "disc",
  222. ]: # Wait for any operations to finish.
  223. rsp = self._getResp(["stat", "err"], timeout)
  224. if rsp and rsp["rsp"][0] == "err":
  225. errcode = rsp["code"][0]
  226. _LOGGER.debug(
  227. "Error trying to connect to peripheral %s, error: %s",
  228. self._mac,
  229. errcode,
  230. )
  231. if errcode == "connfail": # not terminal, can retry connection.
  232. return self._connect(retry - 1, timeout)
  233. if errcode == "nomgmt":
  234. raise bluepy.btle.BTLEManagementError(
  235. "Management not available (permissions problem?)", rsp
  236. )
  237. if errcode == "atterr":
  238. raise bluepy.btle.BTLEGattError("Bluetooth command failed", rsp)
  239. raise bluepy.btle.BTLEException(
  240. "Error from bluepy-helper (%s)" % errcode, rsp
  241. )
  242. if not rsp or rsp["state"][0] != "conn":
  243. _LOGGER.warning("Bluehelper returned unable to connect state: %s", rsp)
  244. self._stopHelper()
  245. if rsp is None:
  246. raise bluepy.btle.BTLEDisconnectError(
  247. "Timed out while trying to connect to peripheral %s" % self._mac,
  248. rsp,
  249. )
  250. raise bluepy.btle.BTLEDisconnectError(
  251. "Failed to connect to peripheral %s, rsp: %s" % (self._mac, rsp)
  252. )
  253. def _commandkey(self, key: str) -> str:
  254. if self._password_encoded is None:
  255. return key
  256. key_action = key[3]
  257. key_suffix = key[4:]
  258. return KEY_PASSWORD_PREFIX + key_action + self._password_encoded + key_suffix
  259. def _writekey(self, key: str) -> bool:
  260. _LOGGER.debug("Prepare to send")
  261. if self._helper is None or self.getState() == "disc":
  262. return False
  263. try:
  264. hand = self.getCharacteristics(uuid=_sb_uuid("tx"))[0]
  265. _LOGGER.debug("Sending command, %s", key)
  266. hand.write(bytes.fromhex(key), withResponse=False)
  267. except bluepy.btle.BTLEException:
  268. _LOGGER.warning("Error sending command to Switchbot", exc_info=True)
  269. raise
  270. else:
  271. _LOGGER.info("Successfully sent command to Switchbot (MAC: %s)", self._mac)
  272. return True
  273. def _subscribe(self) -> bool:
  274. _LOGGER.debug("Subscribe to notifications")
  275. if self._helper is None or self.getState() == "disc":
  276. return False
  277. enable_notify_flag = b"\x01\x00" # standard gatt flag to enable notification
  278. try:
  279. handle = self.getCharacteristics(uuid=_sb_uuid("rx"))[0]
  280. notify_handle = handle.getHandle() + 1
  281. self.writeCharacteristic(
  282. notify_handle, enable_notify_flag, withResponse=False
  283. )
  284. except bluepy.btle.BTLEException:
  285. _LOGGER.warning(
  286. "Error while enabling notifications on Switchbot", exc_info=True
  287. )
  288. raise
  289. return True
  290. def _readkey(self) -> bytes:
  291. _LOGGER.debug("Prepare to read notification from switchbot")
  292. if self._helper is None:
  293. return b"\x00"
  294. try:
  295. receive_handle = self.getCharacteristics(uuid=_sb_uuid("rx"))
  296. except bluepy.btle.BTLEException:
  297. _LOGGER.warning(
  298. "Error while reading notifications from Switchbot", exc_info=True
  299. )
  300. else:
  301. for char in receive_handle:
  302. read_result: bytes = char.read()
  303. return read_result
  304. # Could disconnect before reading response. Assume it worked as this is executed after issueing command.
  305. if self._helper and self.getState() == "disc":
  306. return b"\x01"
  307. return b"\x00"
  308. def _sendcommand(self, key: str, retry: int, timeout: int | None = None) -> bytes:
  309. command = self._commandkey(key)
  310. send_success = False
  311. notify_msg = None
  312. _LOGGER.debug("Sending command to switchbot %s", command)
  313. if len(self._mac.split(":")) != 6:
  314. raise ValueError("Expected MAC address, got %s" % repr(self._mac))
  315. with CONNECT_LOCK:
  316. try:
  317. self._connect(retry, timeout)
  318. send_success = self._subscribe()
  319. except bluepy.btle.BTLEException:
  320. _LOGGER.warning("Error connecting to Switchbot", exc_info=True)
  321. else:
  322. try:
  323. send_success = self._writekey(command)
  324. except bluepy.btle.BTLEException:
  325. _LOGGER.warning(
  326. "Error sending commands to Switchbot", exc_info=True
  327. )
  328. else:
  329. notify_msg = self._readkey()
  330. finally:
  331. self.disconnect()
  332. if notify_msg and send_success:
  333. if notify_msg == b"\x07":
  334. _LOGGER.error("Password required")
  335. elif notify_msg == b"\t":
  336. _LOGGER.error("Password incorrect")
  337. return notify_msg
  338. if retry < 1:
  339. _LOGGER.error(
  340. "Switchbot communication failed. Stopping trying", exc_info=True
  341. )
  342. return b"\x00"
  343. _LOGGER.warning("Cannot connect to Switchbot. Retrying (remaining: %d)", retry)
  344. time.sleep(DEFAULT_RETRY_TIMEOUT)
  345. return self._sendcommand(key, retry - 1)
  346. def get_mac(self) -> str:
  347. """Return mac address of device."""
  348. return self._mac
  349. def get_battery_percent(self) -> Any:
  350. """Return device battery level in percent."""
  351. if not self._sb_adv_data.get("data"):
  352. return None
  353. return self._sb_adv_data["data"].get("battery")
  354. def get_device_data(
  355. self,
  356. retry: int = DEFAULT_RETRY_COUNT,
  357. interface: int | None = None,
  358. passive: bool = False,
  359. ) -> dict[str, Any]:
  360. """Find switchbot devices and their advertisement data."""
  361. _interface: int = interface if interface else self._interface
  362. dev_id = self._mac.replace(":", "")
  363. _data = GetSwitchbotDevices(interface=_interface).discover(
  364. retry=retry,
  365. scan_timeout=self._scan_timeout,
  366. passive=passive,
  367. mac=self._mac,
  368. )
  369. if _data.get(dev_id):
  370. self._sb_adv_data = _data[dev_id]
  371. return self._sb_adv_data
  372. class Switchbot(SwitchbotDevice):
  373. """Representation of a Switchbot."""
  374. def __init__(self, *args: Any, **kwargs: Any) -> None:
  375. """Switchbot Bot/WoHand constructor."""
  376. super().__init__(*args, **kwargs)
  377. self._inverse: bool = kwargs.pop("inverse_mode", False)
  378. self._settings: dict[str, Any] = {}
  379. def update(self, interface: int | None = None, passive: bool = False) -> None:
  380. """Update mode, battery percent and state of device."""
  381. self.get_device_data(
  382. retry=self._retry_count, interface=interface, passive=passive
  383. )
  384. def turn_on(self) -> bool:
  385. """Turn device on."""
  386. result = self._sendcommand(ON_KEY, self._retry_count)
  387. if result[0] == 1:
  388. return True
  389. if result[0] == 5:
  390. _LOGGER.debug("Bot is in press mode and doesn't have on state")
  391. return True
  392. return False
  393. def turn_off(self) -> bool:
  394. """Turn device off."""
  395. result = self._sendcommand(OFF_KEY, self._retry_count)
  396. if result[0] == 1:
  397. return True
  398. if result[0] == 5:
  399. _LOGGER.debug("Bot is in press mode and doesn't have off state")
  400. return True
  401. return False
  402. def hand_up(self) -> bool:
  403. """Raise device arm."""
  404. result = self._sendcommand(UP_KEY, self._retry_count)
  405. if result[0] == 1:
  406. return True
  407. if result[0] == 5:
  408. _LOGGER.debug("Bot is in press mode")
  409. return True
  410. return False
  411. def hand_down(self) -> bool:
  412. """Lower device arm."""
  413. result = self._sendcommand(DOWN_KEY, self._retry_count)
  414. if result[0] == 1:
  415. return True
  416. if result[0] == 5:
  417. _LOGGER.debug("Bot is in press mode")
  418. return True
  419. return False
  420. def press(self) -> bool:
  421. """Press command to device."""
  422. result = self._sendcommand(PRESS_KEY, self._retry_count)
  423. if result[0] == 1:
  424. return True
  425. if result[0] == 5:
  426. _LOGGER.debug("Bot is in switch mode")
  427. return True
  428. return False
  429. def set_switch_mode(
  430. self, switch_mode: bool = False, strength: int = 100, inverse: bool = False
  431. ) -> bool:
  432. """Change bot mode."""
  433. mode_key = format(switch_mode, "b") + format(inverse, "b")
  434. strength_key = f"{strength:0{2}x}" # to hex with padding to double digit
  435. result = self._sendcommand(
  436. DEVICE_SET_MODE_KEY + strength_key + mode_key, self._retry_count
  437. )
  438. if result[0] == 1:
  439. return True
  440. return False
  441. def set_long_press(self, duration: int = 0) -> bool:
  442. """Set bot long press duration."""
  443. duration_key = f"{duration:0{2}x}" # to hex with padding to double digit
  444. result = self._sendcommand(
  445. DEVICE_SET_EXTENDED_KEY + "08" + duration_key, self._retry_count
  446. )
  447. if result[0] == 1:
  448. return True
  449. return False
  450. def get_basic_info(self) -> dict[str, Any] | None:
  451. """Get device basic settings."""
  452. _data = self._sendcommand(
  453. key=DEVICE_GET_BASIC_SETTINGS_KEY, retry=self._retry_count
  454. )
  455. if _data in (b"\x07", b"\x00"):
  456. _LOGGER.error("Unsuccessfull, please try again")
  457. return None
  458. self._settings = {
  459. "battery": _data[1],
  460. "firmware": _data[2] / 10.0,
  461. "strength": _data[3],
  462. "timers": _data[8],
  463. "switchMode": bool(_data[9] & 16),
  464. "inverseDirection": bool(_data[9] & 1),
  465. "holdSeconds": _data[10],
  466. }
  467. return self._settings
  468. def switch_mode(self) -> Any:
  469. """Return true or false from cache."""
  470. # To get actual position call update() first.
  471. if not self._sb_adv_data.get("data"):
  472. return None
  473. return self._sb_adv_data.get("switchMode")
  474. def is_on(self) -> Any:
  475. """Return switch state from cache."""
  476. # To get actual position call update() first.
  477. if not self._sb_adv_data.get("data"):
  478. return None
  479. if self._inverse:
  480. return not self._sb_adv_data["data"].get("isOn")
  481. return self._sb_adv_data["data"].get("isOn")
  482. class SwitchbotCurtain(SwitchbotDevice):
  483. """Representation of a Switchbot Curtain."""
  484. def __init__(self, *args: Any, **kwargs: Any) -> None:
  485. """Switchbot Curtain/WoCurtain constructor."""
  486. # The position of the curtain is saved returned with 0 = open and 100 = closed.
  487. # This is independent of the calibration of the curtain bot (Open left to right/
  488. # Open right to left/Open from the middle).
  489. # The parameter 'reverse_mode' reverse these values,
  490. # if 'reverse_mode' = True, position = 0 equals close
  491. # and position = 100 equals open. The parameter is default set to True so that
  492. # the definition of position is the same as in Home Assistant.
  493. super().__init__(*args, **kwargs)
  494. self._reverse: bool = kwargs.pop("reverse_mode", True)
  495. self._settings: dict[str, Any] = {}
  496. self.ext_info_sum: dict[str, Any] = {}
  497. self.ext_info_adv: dict[str, Any] = {}
  498. def open(self) -> bool:
  499. """Send open command."""
  500. result = self._sendcommand(OPEN_KEY, self._retry_count)
  501. if result[0] == 1:
  502. return True
  503. return False
  504. def close(self) -> bool:
  505. """Send close command."""
  506. result = self._sendcommand(CLOSE_KEY, self._retry_count)
  507. if result[0] == 1:
  508. return True
  509. return False
  510. def stop(self) -> bool:
  511. """Send stop command to device."""
  512. result = self._sendcommand(STOP_KEY, self._retry_count)
  513. if result[0] == 1:
  514. return True
  515. return False
  516. def set_position(self, position: int) -> bool:
  517. """Send position command (0-100) to device."""
  518. position = (100 - position) if self._reverse else position
  519. hex_position = "%0.2X" % position
  520. result = self._sendcommand(POSITION_KEY + hex_position, self._retry_count)
  521. if result[0] == 1:
  522. return True
  523. return False
  524. def update(self, interface: int | None = None, passive: bool = False) -> None:
  525. """Update position, battery percent and light level of device."""
  526. self.get_device_data(
  527. retry=self._retry_count, interface=interface, passive=passive
  528. )
  529. def get_position(self) -> Any:
  530. """Return cached position (0-100) of Curtain."""
  531. # To get actual position call update() first.
  532. if not self._sb_adv_data.get("data"):
  533. return None
  534. return self._sb_adv_data["data"].get("position")
  535. def get_basic_info(self) -> dict[str, Any] | None:
  536. """Get device basic settings."""
  537. _data = self._sendcommand(
  538. key=DEVICE_GET_BASIC_SETTINGS_KEY, retry=self._retry_count
  539. )
  540. if _data in (b"\x07", b"\x00"):
  541. _LOGGER.error("Unsuccessfull, please try again")
  542. return None
  543. _position = max(min(_data[6], 100), 0)
  544. self._settings = {
  545. "battery": _data[1],
  546. "firmware": _data[2] / 10.0,
  547. "chainLength": _data[3],
  548. "openDirection": (
  549. "right_to_left" if _data[4] & 0b10000000 == 128 else "left_to_right"
  550. ),
  551. "touchToOpen": bool(_data[4] & 0b01000000),
  552. "light": bool(_data[4] & 0b00100000),
  553. "fault": bool(_data[4] & 0b00001000),
  554. "solarPanel": bool(_data[5] & 0b00001000),
  555. "calibrated": bool(_data[5] & 0b00000100),
  556. "inMotion": bool(_data[5] & 0b01000011),
  557. "position": (100 - _position) if self._reverse else _position,
  558. "timers": _data[7],
  559. }
  560. return self._settings
  561. def get_extended_info_summary(self) -> dict[str, Any] | None:
  562. """Get basic info for all devices in chain."""
  563. _data = self._sendcommand(key=CURTAIN_EXT_SUM_KEY, retry=self._retry_count)
  564. if _data in (b"\x07", b"\x00"):
  565. _LOGGER.error("Unsuccessfull, please try again")
  566. return None
  567. self.ext_info_sum["device0"] = {
  568. "openDirectionDefault": not bool(_data[1] & 0b10000000),
  569. "touchToOpen": bool(_data[1] & 0b01000000),
  570. "light": bool(_data[1] & 0b00100000),
  571. "openDirection": (
  572. "left_to_right" if _data[1] & 0b00010000 == 1 else "right_to_left"
  573. ),
  574. }
  575. # if grouped curtain device present.
  576. if _data[2] != 0:
  577. self.ext_info_sum["device1"] = {
  578. "openDirectionDefault": not bool(_data[1] & 0b10000000),
  579. "touchToOpen": bool(_data[1] & 0b01000000),
  580. "light": bool(_data[1] & 0b00100000),
  581. "openDirection": (
  582. "left_to_right" if _data[1] & 0b00010000 else "right_to_left"
  583. ),
  584. }
  585. return self.ext_info_sum
  586. def get_extended_info_adv(self) -> dict[str, Any] | None:
  587. """Get advance page info for device chain."""
  588. _data = self._sendcommand(key=CURTAIN_EXT_ADV_KEY, retry=self._retry_count)
  589. if _data in (b"\x07", b"\x00"):
  590. _LOGGER.error("Unsuccessfull, please try again")
  591. return None
  592. _state_of_charge = [
  593. "not_charging",
  594. "charging_by_adapter",
  595. "charging_by_solar",
  596. "fully_charged",
  597. "solar_not_charging",
  598. "charging_error",
  599. ]
  600. self.ext_info_adv["device0"] = {
  601. "battery": _data[1],
  602. "firmware": _data[2] / 10.0,
  603. "stateOfCharge": _state_of_charge[_data[3]],
  604. }
  605. # If grouped curtain device present.
  606. if _data[4]:
  607. self.ext_info_adv["device1"] = {
  608. "battery": _data[4],
  609. "firmware": _data[5] / 10.0,
  610. "stateOfCharge": _state_of_charge[_data[6]],
  611. }
  612. return self.ext_info_adv
  613. def get_light_level(self) -> Any:
  614. """Return cached light level."""
  615. # To get actual light level call update() first.
  616. if not self._sb_adv_data.get("data"):
  617. return None
  618. return self._sb_adv_data["data"].get("lightLevel")
  619. def is_reversed(self) -> bool:
  620. """Return True if curtain position is opposite from SB data."""
  621. return self._reverse
  622. def is_calibrated(self) -> Any:
  623. """Return True curtain is calibrated."""
  624. # To get actual light level call update() first.
  625. if not self._sb_adv_data.get("data"):
  626. return None
  627. return self._sb_adv_data["data"].get("calibration")