adv_parser.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902
  1. """Library to handle connection with Switchbot."""
  2. from __future__ import annotations
  3. import logging
  4. from collections import defaultdict
  5. from collections.abc import Callable
  6. from functools import lru_cache
  7. from typing import Any, TypedDict
  8. from bleak.backends.device import BLEDevice
  9. from bleak.backends.scanner import AdvertisementData
  10. from .adv_parsers.air_purifier import process_air_purifier
  11. from .adv_parsers.art_frame import process_art_frame
  12. from .adv_parsers.blind_tilt import process_woblindtilt
  13. from .adv_parsers.bot import process_wohand
  14. from .adv_parsers.bulb import process_color_bulb
  15. from .adv_parsers.ceiling_light import process_woceiling
  16. from .adv_parsers.climate_panel import process_climate_panel
  17. from .adv_parsers.contact import process_wocontact
  18. from .adv_parsers.curtain import process_wocurtain
  19. from .adv_parsers.fan import process_fan
  20. from .adv_parsers.hub2 import process_wohub2
  21. from .adv_parsers.hub3 import process_hub3
  22. from .adv_parsers.hubmini_matter import process_hubmini_matter
  23. from .adv_parsers.humidifier import process_evaporative_humidifier, process_wohumidifier
  24. from .adv_parsers.keypad import process_wokeypad
  25. from .adv_parsers.leak import process_leak
  26. from .adv_parsers.light_strip import process_light, process_rgbic_light, process_wostrip
  27. from .adv_parsers.lock import (
  28. process_lock2,
  29. process_locklite,
  30. process_wolock,
  31. process_wolock_pro,
  32. )
  33. from .adv_parsers.meter import process_wosensorth, process_wosensorth_c
  34. from .adv_parsers.motion import process_wopresence
  35. from .adv_parsers.plug import process_woplugmini
  36. from .adv_parsers.presence_sensor import process_presence_sensor
  37. from .adv_parsers.relay_switch import (
  38. process_garage_door_opener,
  39. process_relay_switch_1pm,
  40. process_relay_switch_2pm,
  41. process_relay_switch_common_data,
  42. )
  43. from .adv_parsers.remote import process_woremote
  44. from .adv_parsers.roller_shade import process_worollershade
  45. from .adv_parsers.smart_thermostat_radiator import process_smart_thermostat_radiator
  46. from .adv_parsers.vacuum import process_vacuum, process_vacuum_k
  47. from .const import SwitchbotModel
  48. from .models import SwitchBotAdvertisement
  49. from .utils import format_mac_upper
  50. _LOGGER = logging.getLogger(__name__)
  51. SERVICE_DATA_ORDER = (
  52. "0000fd3d-0000-1000-8000-00805f9b34fb",
  53. "00000d00-0000-1000-8000-00805f9b34fb",
  54. )
  55. MFR_DATA_ORDER = (2409, 741, 89)
  56. _MODEL_TO_MAC_CACHE: dict[str, SwitchbotModel] = {}
  57. class SwitchbotSupportedType(TypedDict):
  58. """Supported type of Switchbot."""
  59. modelName: SwitchbotModel
  60. modelFriendlyName: str
  61. func: Callable[[bytes, bytes | None], dict[str, bool | int]]
  62. manufacturer_id: int | None
  63. manufacturer_data_length: int | None
  64. SUPPORTED_TYPES: dict[str | bytes, SwitchbotSupportedType] = {
  65. "d": {
  66. "modelName": SwitchbotModel.CONTACT_SENSOR,
  67. "modelFriendlyName": "Contact Sensor",
  68. "func": process_wocontact,
  69. "manufacturer_id": 2409,
  70. },
  71. "D": {
  72. "modelName": SwitchbotModel.CONTACT_SENSOR,
  73. "modelFriendlyName": "Contact Sensor",
  74. "func": process_wocontact,
  75. "manufacturer_id": 2409,
  76. },
  77. "H": {
  78. "modelName": SwitchbotModel.BOT,
  79. "modelFriendlyName": "Bot",
  80. "func": process_wohand,
  81. "manufacturer_id": 89,
  82. },
  83. "s": {
  84. "modelName": SwitchbotModel.MOTION_SENSOR,
  85. "modelFriendlyName": "Motion Sensor",
  86. "func": process_wopresence,
  87. "manufacturer_id": 2409,
  88. },
  89. "S": {
  90. "modelName": SwitchbotModel.MOTION_SENSOR,
  91. "modelFriendlyName": "Motion Sensor",
  92. "func": process_wopresence,
  93. "manufacturer_id": 2409,
  94. },
  95. "r": {
  96. "modelName": SwitchbotModel.LIGHT_STRIP,
  97. "modelFriendlyName": "Light Strip",
  98. "func": process_wostrip,
  99. "manufacturer_id": 2409,
  100. },
  101. "R": {
  102. "modelName": SwitchbotModel.LIGHT_STRIP,
  103. "modelFriendlyName": "Light Strip",
  104. "func": process_wostrip,
  105. "manufacturer_id": 2409,
  106. },
  107. "{": {
  108. "modelName": SwitchbotModel.CURTAIN,
  109. "modelFriendlyName": "Curtain 3",
  110. "func": process_wocurtain,
  111. "manufacturer_id": 2409,
  112. },
  113. "[": {
  114. "modelName": SwitchbotModel.CURTAIN,
  115. "modelFriendlyName": "Curtain 3",
  116. "func": process_wocurtain,
  117. "manufacturer_id": 2409,
  118. },
  119. "c": {
  120. "modelName": SwitchbotModel.CURTAIN,
  121. "modelFriendlyName": "Curtain",
  122. "func": process_wocurtain,
  123. "manufacturer_id": 2409,
  124. },
  125. "C": {
  126. "modelName": SwitchbotModel.CURTAIN,
  127. "modelFriendlyName": "Curtain",
  128. "func": process_wocurtain,
  129. "manufacturer_id": 2409,
  130. },
  131. "w": {
  132. "modelName": SwitchbotModel.IO_METER,
  133. "modelFriendlyName": "Indoor/Outdoor Meter",
  134. "func": process_wosensorth,
  135. "manufacturer_id": 2409,
  136. },
  137. "W": {
  138. "modelName": SwitchbotModel.IO_METER,
  139. "modelFriendlyName": "Indoor/Outdoor Meter",
  140. "func": process_wosensorth,
  141. "manufacturer_id": 2409,
  142. },
  143. "i": {
  144. "modelName": SwitchbotModel.METER,
  145. "modelFriendlyName": "Meter Plus",
  146. "func": process_wosensorth,
  147. "manufacturer_id": 2409,
  148. },
  149. "I": {
  150. "modelName": SwitchbotModel.METER,
  151. "modelFriendlyName": "Meter Plus",
  152. "func": process_wosensorth,
  153. "manufacturer_id": 2409,
  154. },
  155. "T": {
  156. "modelName": SwitchbotModel.METER,
  157. "modelFriendlyName": "Meter",
  158. "func": process_wosensorth,
  159. "manufacturer_id": 2409,
  160. },
  161. "t": {
  162. "modelName": SwitchbotModel.METER,
  163. "modelFriendlyName": "Meter",
  164. "func": process_wosensorth,
  165. "manufacturer_id": 2409,
  166. },
  167. "4": {
  168. "modelName": SwitchbotModel.METER_PRO,
  169. "modelFriendlyName": "Meter Pro",
  170. "func": process_wosensorth,
  171. "manufacturer_id": 2409,
  172. },
  173. b"\x14": {
  174. "modelName": SwitchbotModel.METER_PRO,
  175. "modelFriendlyName": "Meter Pro",
  176. "func": process_wosensorth,
  177. "manufacturer_id": 2409,
  178. },
  179. "5": {
  180. "modelName": SwitchbotModel.METER_PRO_C,
  181. "modelFriendlyName": "Meter Pro CO2",
  182. "func": process_wosensorth_c,
  183. "manufacturer_id": 2409,
  184. },
  185. b"\x15": {
  186. "modelName": SwitchbotModel.METER_PRO_C,
  187. "modelFriendlyName": "Meter Pro CO2",
  188. "func": process_wosensorth_c,
  189. "manufacturer_id": 2409,
  190. },
  191. "v": {
  192. "modelName": SwitchbotModel.HUB2,
  193. "modelFriendlyName": "Hub 2",
  194. "func": process_wohub2,
  195. "manufacturer_id": 2409,
  196. },
  197. "V": {
  198. "modelName": SwitchbotModel.HUB2,
  199. "modelFriendlyName": "Hub 2",
  200. "func": process_wohub2,
  201. "manufacturer_id": 2409,
  202. },
  203. "g": {
  204. "modelName": SwitchbotModel.PLUG_MINI,
  205. "modelFriendlyName": "Plug Mini",
  206. "func": process_woplugmini,
  207. "manufacturer_id": 2409,
  208. },
  209. "G": {
  210. "modelName": SwitchbotModel.PLUG_MINI,
  211. "modelFriendlyName": "Plug Mini",
  212. "func": process_woplugmini,
  213. "manufacturer_id": 2409,
  214. },
  215. "j": {
  216. "modelName": SwitchbotModel.PLUG_MINI,
  217. "modelFriendlyName": "Plug Mini (JP)",
  218. "func": process_woplugmini,
  219. "manufacturer_id": 2409,
  220. },
  221. "J": {
  222. "modelName": SwitchbotModel.PLUG_MINI,
  223. "modelFriendlyName": "Plug Mini (JP)",
  224. "func": process_woplugmini,
  225. "manufacturer_id": 2409,
  226. },
  227. "u": {
  228. "modelName": SwitchbotModel.COLOR_BULB,
  229. "modelFriendlyName": "Color Bulb",
  230. "func": process_color_bulb,
  231. "manufacturer_id": 2409,
  232. },
  233. "U": {
  234. "modelName": SwitchbotModel.COLOR_BULB,
  235. "modelFriendlyName": "Color Bulb",
  236. "func": process_color_bulb,
  237. "manufacturer_id": 2409,
  238. },
  239. "q": {
  240. "modelName": SwitchbotModel.CEILING_LIGHT,
  241. "modelFriendlyName": "Ceiling Light",
  242. "func": process_woceiling,
  243. "manufacturer_id": 2409,
  244. },
  245. "Q": {
  246. "modelName": SwitchbotModel.CEILING_LIGHT,
  247. "modelFriendlyName": "Ceiling Light",
  248. "func": process_woceiling,
  249. "manufacturer_id": 2409,
  250. },
  251. "n": {
  252. "modelName": SwitchbotModel.CEILING_LIGHT,
  253. "modelFriendlyName": "Ceiling Light Pro",
  254. "func": process_woceiling,
  255. "manufacturer_id": 2409,
  256. },
  257. "N": {
  258. "modelName": SwitchbotModel.CEILING_LIGHT,
  259. "modelFriendlyName": "Ceiling Light Pro",
  260. "func": process_woceiling,
  261. "manufacturer_id": 2409,
  262. },
  263. "e": {
  264. "modelName": SwitchbotModel.HUMIDIFIER,
  265. "modelFriendlyName": "Humidifier",
  266. "func": process_wohumidifier,
  267. "manufacturer_id": 741,
  268. "manufacturer_data_length": 6,
  269. },
  270. "E": {
  271. "modelName": SwitchbotModel.HUMIDIFIER,
  272. "modelFriendlyName": "Humidifier",
  273. "func": process_wohumidifier,
  274. "manufacturer_id": 741,
  275. "manufacturer_data_length": 6,
  276. },
  277. "#": {
  278. "modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  279. "modelFriendlyName": "Evaporative Humidifier",
  280. "func": process_evaporative_humidifier,
  281. "manufacturer_id": 2409,
  282. },
  283. b"\x03": {
  284. "modelName": SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  285. "modelFriendlyName": "Evaporative Humidifier",
  286. "func": process_evaporative_humidifier,
  287. "manufacturer_id": 2409,
  288. },
  289. "o": {
  290. "modelName": SwitchbotModel.LOCK,
  291. "modelFriendlyName": "Lock",
  292. "func": process_wolock,
  293. "manufacturer_id": 2409,
  294. },
  295. "O": {
  296. "modelName": SwitchbotModel.LOCK,
  297. "modelFriendlyName": "Lock",
  298. "func": process_wolock,
  299. "manufacturer_id": 2409,
  300. },
  301. "$": {
  302. "modelName": SwitchbotModel.LOCK_PRO,
  303. "modelFriendlyName": "Lock Pro",
  304. "func": process_wolock_pro,
  305. "manufacturer_id": 2409,
  306. },
  307. b"\x04": {
  308. "modelName": SwitchbotModel.LOCK_PRO,
  309. "modelFriendlyName": "Lock Pro",
  310. "func": process_wolock_pro,
  311. "manufacturer_id": 2409,
  312. },
  313. "x": {
  314. "modelName": SwitchbotModel.BLIND_TILT,
  315. "modelFriendlyName": "Blind Tilt",
  316. "func": process_woblindtilt,
  317. "manufacturer_id": 2409,
  318. },
  319. "X": {
  320. "modelName": SwitchbotModel.BLIND_TILT,
  321. "modelFriendlyName": "Blind Tilt",
  322. "func": process_woblindtilt,
  323. "manufacturer_id": 2409,
  324. },
  325. "&": {
  326. "modelName": SwitchbotModel.LEAK,
  327. "modelFriendlyName": "Leak Detector",
  328. "func": process_leak,
  329. "manufacturer_id": 2409,
  330. },
  331. b"\x06": {
  332. "modelName": SwitchbotModel.LEAK,
  333. "modelFriendlyName": "Leak Detector",
  334. "func": process_leak,
  335. "manufacturer_id": 2409,
  336. },
  337. "y": {
  338. "modelName": SwitchbotModel.KEYPAD,
  339. "modelFriendlyName": "Keypad",
  340. "func": process_wokeypad,
  341. "manufacturer_id": 2409,
  342. },
  343. "Y": {
  344. "modelName": SwitchbotModel.KEYPAD,
  345. "modelFriendlyName": "Keypad",
  346. "func": process_wokeypad,
  347. "manufacturer_id": 2409,
  348. },
  349. "<": {
  350. "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
  351. "modelFriendlyName": "Relay Switch 1PM",
  352. "func": process_relay_switch_1pm,
  353. "manufacturer_id": 2409,
  354. },
  355. b"\x1c": {
  356. "modelName": SwitchbotModel.RELAY_SWITCH_1PM,
  357. "modelFriendlyName": "Relay Switch 1PM",
  358. "func": process_relay_switch_1pm,
  359. "manufacturer_id": 2409,
  360. },
  361. ";": {
  362. "modelName": SwitchbotModel.RELAY_SWITCH_1,
  363. "modelFriendlyName": "Relay Switch 1",
  364. "func": process_relay_switch_common_data,
  365. "manufacturer_id": 2409,
  366. },
  367. b"\x1b": {
  368. "modelName": SwitchbotModel.RELAY_SWITCH_1,
  369. "modelFriendlyName": "Relay Switch 1",
  370. "func": process_relay_switch_common_data,
  371. "manufacturer_id": 2409,
  372. },
  373. "b": {
  374. "modelName": SwitchbotModel.REMOTE,
  375. "modelFriendlyName": "Remote",
  376. "func": process_woremote,
  377. "manufacturer_id": 89,
  378. },
  379. "B": {
  380. "modelName": SwitchbotModel.REMOTE,
  381. "modelFriendlyName": "Remote",
  382. "func": process_woremote,
  383. "manufacturer_id": 89,
  384. },
  385. ",": {
  386. "modelName": SwitchbotModel.ROLLER_SHADE,
  387. "modelFriendlyName": "Roller Shade",
  388. "func": process_worollershade,
  389. "manufacturer_id": 2409,
  390. },
  391. b"\x0c": {
  392. "modelName": SwitchbotModel.ROLLER_SHADE,
  393. "modelFriendlyName": "Roller Shade",
  394. "func": process_worollershade,
  395. "manufacturer_id": 2409,
  396. },
  397. "%": {
  398. "modelName": SwitchbotModel.HUBMINI_MATTER,
  399. "modelFriendlyName": "HubMini Matter",
  400. "func": process_hubmini_matter,
  401. "manufacturer_id": 2409,
  402. },
  403. b"\x05": {
  404. "modelName": SwitchbotModel.HUBMINI_MATTER,
  405. "modelFriendlyName": "HubMini Matter",
  406. "func": process_hubmini_matter,
  407. "manufacturer_id": 2409,
  408. },
  409. "~": {
  410. "modelName": SwitchbotModel.CIRCULATOR_FAN,
  411. "modelFriendlyName": "Circulator Fan",
  412. "func": process_fan,
  413. "manufacturer_id": 2409,
  414. },
  415. "^": {
  416. "modelName": SwitchbotModel.CIRCULATOR_FAN,
  417. "modelFriendlyName": "Circulator Fan",
  418. "func": process_fan,
  419. "manufacturer_id": 2409,
  420. },
  421. ".": {
  422. "modelName": SwitchbotModel.K20_VACUUM,
  423. "modelFriendlyName": "K20 Vacuum",
  424. "func": process_vacuum,
  425. "manufacturer_id": 2409,
  426. },
  427. b"\x0f": {
  428. "modelName": SwitchbotModel.K20_VACUUM,
  429. "modelFriendlyName": "K20 Vacuum",
  430. "func": process_vacuum,
  431. "manufacturer_id": 2409,
  432. },
  433. "z": {
  434. "modelName": SwitchbotModel.S10_VACUUM,
  435. "modelFriendlyName": "S10 Vacuum",
  436. "func": process_vacuum,
  437. "manufacturer_id": 2409,
  438. },
  439. "Z": {
  440. "modelName": SwitchbotModel.S10_VACUUM,
  441. "modelFriendlyName": "S10 Vacuum",
  442. "func": process_vacuum,
  443. "manufacturer_id": 2409,
  444. },
  445. "3": {
  446. "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
  447. "modelFriendlyName": "K10+ Pro Combo Vacuum",
  448. "func": process_vacuum,
  449. "manufacturer_id": 2409,
  450. },
  451. b"\x13": {
  452. "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
  453. "modelFriendlyName": "K10+ Pro Combo Vacuum",
  454. "func": process_vacuum,
  455. "manufacturer_id": 2409,
  456. },
  457. "}": {
  458. "modelName": SwitchbotModel.K10_VACUUM,
  459. "modelFriendlyName": "K10+ Vacuum",
  460. "func": process_vacuum_k,
  461. "manufacturer_id": 2409,
  462. },
  463. "]": {
  464. "modelName": SwitchbotModel.K10_VACUUM,
  465. "modelFriendlyName": "K10+ Vacuum",
  466. "func": process_vacuum_k,
  467. "manufacturer_id": 2409,
  468. },
  469. "(": {
  470. "modelName": SwitchbotModel.K10_PRO_VACUUM,
  471. "modelFriendlyName": "K10+ Pro Vacuum",
  472. "func": process_vacuum_k,
  473. "manufacturer_id": 2409,
  474. },
  475. b"\x08": {
  476. "modelName": SwitchbotModel.K10_PRO_VACUUM,
  477. "modelFriendlyName": "K10+ Pro Vacuum",
  478. "func": process_vacuum_k,
  479. "manufacturer_id": 2409,
  480. },
  481. "*": {
  482. "modelName": SwitchbotModel.AIR_PURIFIER,
  483. "modelFriendlyName": "Air Purifier",
  484. "func": process_air_purifier,
  485. "manufacturer_id": 2409,
  486. },
  487. b"\x0a": {
  488. "modelName": SwitchbotModel.AIR_PURIFIER,
  489. "modelFriendlyName": "Air Purifier",
  490. "func": process_air_purifier,
  491. "manufacturer_id": 2409,
  492. },
  493. "+": {
  494. "modelName": SwitchbotModel.AIR_PURIFIER,
  495. "modelFriendlyName": "Air Purifier",
  496. "func": process_air_purifier,
  497. "manufacturer_id": 2409,
  498. },
  499. b"\x0b": {
  500. "modelName": SwitchbotModel.AIR_PURIFIER,
  501. "modelFriendlyName": "Air Purifier",
  502. "func": process_air_purifier,
  503. "manufacturer_id": 2409,
  504. },
  505. "7": {
  506. "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
  507. "modelFriendlyName": "Air Purifier Table",
  508. "func": process_air_purifier,
  509. "manufacturer_id": 2409,
  510. },
  511. b"\x17": {
  512. "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
  513. "modelFriendlyName": "Air Purifier Table",
  514. "func": process_air_purifier,
  515. "manufacturer_id": 2409,
  516. },
  517. "8": {
  518. "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
  519. "modelFriendlyName": "Air Purifier Table",
  520. "func": process_air_purifier,
  521. "manufacturer_id": 2409,
  522. },
  523. b"\x18": {
  524. "modelName": SwitchbotModel.AIR_PURIFIER_TABLE,
  525. "modelFriendlyName": "Air Purifier Table",
  526. "func": process_air_purifier,
  527. "manufacturer_id": 2409,
  528. },
  529. b"\x00\x10\xb9\x40": {
  530. "modelName": SwitchbotModel.HUB3,
  531. "modelFriendlyName": "Hub3",
  532. "func": process_hub3,
  533. "manufacturer_id": 2409,
  534. },
  535. b"\x01\x10\xb9\x40": {
  536. "modelName": SwitchbotModel.HUB3,
  537. "modelFriendlyName": "Hub3",
  538. "func": process_hub3,
  539. "manufacturer_id": 2409,
  540. },
  541. "-": {
  542. "modelName": SwitchbotModel.LOCK_LITE,
  543. "modelFriendlyName": "Lock Lite",
  544. "func": process_locklite,
  545. "manufacturer_id": 2409,
  546. },
  547. b"\x0d": {
  548. "modelName": SwitchbotModel.LOCK_LITE,
  549. "modelFriendlyName": "Lock Lite",
  550. "func": process_locklite,
  551. "manufacturer_id": 2409,
  552. },
  553. b"\x00\x10\xa5\xb8": {
  554. "modelName": SwitchbotModel.LOCK_ULTRA,
  555. "modelFriendlyName": "Lock Ultra",
  556. "func": process_lock2,
  557. "manufacturer_id": 2409,
  558. },
  559. b"\x01\x10\xa5\xb8": {
  560. "modelName": SwitchbotModel.LOCK_ULTRA,
  561. "modelFriendlyName": "Lock Ultra",
  562. "func": process_lock2,
  563. "manufacturer_id": 2409,
  564. },
  565. ">": {
  566. "modelName": SwitchbotModel.GARAGE_DOOR_OPENER,
  567. "modelFriendlyName": "Garage Door Opener",
  568. "func": process_garage_door_opener,
  569. "manufacturer_id": 2409,
  570. },
  571. b"\x1e": {
  572. "modelName": SwitchbotModel.GARAGE_DOOR_OPENER,
  573. "modelFriendlyName": "Garage Door Opener",
  574. "func": process_garage_door_opener,
  575. "manufacturer_id": 2409,
  576. },
  577. "=": {
  578. "modelName": SwitchbotModel.RELAY_SWITCH_2PM,
  579. "modelFriendlyName": "Relay Switch 2PM",
  580. "func": process_relay_switch_2pm,
  581. "manufacturer_id": 2409,
  582. },
  583. b"\x1d": {
  584. "modelName": SwitchbotModel.RELAY_SWITCH_2PM,
  585. "modelFriendlyName": "Relay Switch 2PM",
  586. "func": process_relay_switch_2pm,
  587. "manufacturer_id": 2409,
  588. },
  589. b"\x00\x10\xd0\xb0": {
  590. "modelName": SwitchbotModel.FLOOR_LAMP,
  591. "modelFriendlyName": "Floor Lamp",
  592. "func": process_light,
  593. "manufacturer_id": 2409,
  594. },
  595. b"\x01\x10\xd0\xb0": {
  596. "modelName": SwitchbotModel.FLOOR_LAMP,
  597. "modelFriendlyName": "Floor Lamp",
  598. "func": process_light,
  599. "manufacturer_id": 2409,
  600. },
  601. b"\x00\x10\xd0\xb1": {
  602. "modelName": SwitchbotModel.STRIP_LIGHT_3,
  603. "modelFriendlyName": "Strip Light 3",
  604. "func": process_light,
  605. "manufacturer_id": 2409,
  606. },
  607. b"\x01\x10\xd0\xb1": {
  608. "modelName": SwitchbotModel.STRIP_LIGHT_3,
  609. "modelFriendlyName": "Strip Light 3",
  610. "func": process_light,
  611. "manufacturer_id": 2409,
  612. },
  613. "?": {
  614. "modelName": SwitchbotModel.PLUG_MINI_EU,
  615. "modelFriendlyName": "Plug Mini (EU)",
  616. "func": process_relay_switch_1pm,
  617. "manufacturer_id": 2409,
  618. },
  619. b"\x1f": {
  620. "modelName": SwitchbotModel.PLUG_MINI_EU,
  621. "modelFriendlyName": "Plug Mini (EU)",
  622. "func": process_relay_switch_1pm,
  623. "manufacturer_id": 2409,
  624. },
  625. b"\x00\x10\xd0\xb3": {
  626. "modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT,
  627. "modelFriendlyName": "RGBICWW Strip Light",
  628. "func": process_rgbic_light,
  629. "manufacturer_id": 2409,
  630. },
  631. b"\x01\x10\xd0\xb3": {
  632. "modelName": SwitchbotModel.RGBICWW_STRIP_LIGHT,
  633. "modelFriendlyName": "RGBICWW Strip Light",
  634. "func": process_rgbic_light,
  635. "manufacturer_id": 2409,
  636. },
  637. b"\x00\x10\xd0\xb4": {
  638. "modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP,
  639. "modelFriendlyName": "RGBICWW Floor Lamp",
  640. "func": process_rgbic_light,
  641. "manufacturer_id": 2409,
  642. },
  643. b"\x01\x10\xd0\xb4": {
  644. "modelName": SwitchbotModel.RGBICWW_FLOOR_LAMP,
  645. "modelFriendlyName": "RGBICWW Floor Lamp",
  646. "func": process_rgbic_light,
  647. "manufacturer_id": 2409,
  648. },
  649. b"\x00\x10\xfb\xa8": {
  650. "modelName": SwitchbotModel.K11_VACUUM,
  651. "modelFriendlyName": "K11+ Vacuum",
  652. "func": process_vacuum,
  653. "manufacturer_id": 2409,
  654. },
  655. b"\x01\x10\xfb\xa8": {
  656. "modelName": SwitchbotModel.K11_VACUUM,
  657. "modelFriendlyName": "K11+ Vacuum",
  658. "func": process_vacuum,
  659. "manufacturer_id": 2409,
  660. },
  661. b"\x00\x10\xf3\xd8": {
  662. "modelName": SwitchbotModel.CLIMATE_PANEL,
  663. "modelFriendlyName": "Climate Panel",
  664. "func": process_climate_panel,
  665. "manufacturer_id": 2409,
  666. },
  667. b"\x01\x10\xf3\xd8": {
  668. "modelName": SwitchbotModel.CLIMATE_PANEL,
  669. "modelFriendlyName": "Climate Panel",
  670. "func": process_climate_panel,
  671. "manufacturer_id": 2409,
  672. },
  673. b"\x00\x116@": {
  674. "modelName": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
  675. "modelFriendlyName": "Smart Thermostat Radiator",
  676. "func": process_smart_thermostat_radiator,
  677. "manufacturer_id": 2409,
  678. },
  679. b"\x01\x116@": {
  680. "modelName": SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
  681. "modelFriendlyName": "Smart Thermostat Radiator",
  682. "func": process_smart_thermostat_radiator,
  683. "manufacturer_id": 2409,
  684. },
  685. b"\x00\x10\xe0P": {
  686. "modelName": SwitchbotModel.S20_VACUUM,
  687. "modelFriendlyName": "S20 Vacuum",
  688. "func": process_vacuum,
  689. "manufacturer_id": 2409,
  690. },
  691. b"\x01\x10\xe0P": {
  692. "modelName": SwitchbotModel.S20_VACUUM,
  693. "modelFriendlyName": "S20 Vacuum",
  694. "func": process_vacuum,
  695. "manufacturer_id": 2409,
  696. },
  697. b"\x00\x10\xcc\xc8": {
  698. "modelName": SwitchbotModel.PRESENCE_SENSOR,
  699. "modelFriendlyName": "Presence Sensor",
  700. "func": process_presence_sensor,
  701. "manufacturer_id": 2409,
  702. },
  703. b"\x01\x10\xcc\xc8": {
  704. "modelName": SwitchbotModel.PRESENCE_SENSOR,
  705. "modelFriendlyName": "Presence Sensor",
  706. "func": process_presence_sensor,
  707. "manufacturer_id": 2409,
  708. },
  709. b"\x00\x11>\x10": {
  710. "modelName": SwitchbotModel.ART_FRAME,
  711. "modelFriendlyName": "Art Frame",
  712. "func": process_art_frame,
  713. "manufacturer_id": 2409,
  714. },
  715. b"\x01\x11>\x10": {
  716. "modelName": SwitchbotModel.ART_FRAME,
  717. "modelFriendlyName": "Art Frame",
  718. "func": process_art_frame,
  719. "manufacturer_id": 2409,
  720. },
  721. }
  722. _SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
  723. list
  724. )
  725. for model_chr, model_data in SUPPORTED_TYPES.items():
  726. _SWITCHBOT_MODEL_TO_CHAR[model_data["modelName"]].append(model_chr)
  727. MODELS_BY_MANUFACTURER_DATA: dict[int, list[tuple[str, SwitchbotSupportedType]]] = {
  728. mfr_id: [] for mfr_id in MFR_DATA_ORDER
  729. }
  730. for model_chr, model in SUPPORTED_TYPES.items():
  731. if "manufacturer_id" in model:
  732. mfr_id = model["manufacturer_id"]
  733. MODELS_BY_MANUFACTURER_DATA[mfr_id].append((model_chr, model))
  734. def parse_advertisement_data(
  735. device: BLEDevice,
  736. advertisement_data: AdvertisementData,
  737. model: SwitchbotModel | None = None,
  738. ) -> SwitchBotAdvertisement | None:
  739. """Parse advertisement data."""
  740. upper_mac = format_mac_upper(device.address)
  741. if model is None and upper_mac in _MODEL_TO_MAC_CACHE:
  742. model = _MODEL_TO_MAC_CACHE[upper_mac]
  743. service_data = advertisement_data.service_data
  744. _service_data = None
  745. for uuid in SERVICE_DATA_ORDER:
  746. if uuid in service_data:
  747. _service_data = service_data[uuid]
  748. break
  749. _mfr_data = None
  750. _mfr_id = None
  751. for mfr_id in MFR_DATA_ORDER:
  752. if mfr_id in advertisement_data.manufacturer_data:
  753. _mfr_id = mfr_id
  754. _mfr_data = advertisement_data.manufacturer_data[mfr_id]
  755. break
  756. if _mfr_data is None and _service_data is None:
  757. return None
  758. try:
  759. data = _parse_data(
  760. _service_data,
  761. _mfr_data,
  762. _mfr_id,
  763. model,
  764. )
  765. except Exception: # pylint: disable=broad-except
  766. _LOGGER.exception("Failed to parse advertisement data: %s", advertisement_data)
  767. return None
  768. if not data:
  769. return None
  770. return SwitchBotAdvertisement(
  771. device.address, data, device, advertisement_data.rssi, bool(_service_data)
  772. )
  773. def _find_model_from_service_data(_service_data: bytes) -> str | bytes | None:
  774. """Find model from service data."""
  775. char_model = chr(_service_data[0] & 0b01111111)
  776. if char_model in SUPPORTED_TYPES:
  777. return char_model
  778. byte_model = bytes([_service_data[0] & 0b01111111])
  779. if byte_model in SUPPORTED_TYPES:
  780. return byte_model
  781. return None
  782. def _find_model_from_switchbot_model(
  783. _switchbot_model: SwitchbotModel,
  784. ) -> str | bytes | None:
  785. """Find model from switchbot model."""
  786. if _switchbot_model in _SWITCHBOT_MODEL_TO_CHAR:
  787. return _SWITCHBOT_MODEL_TO_CHAR[_switchbot_model][0]
  788. return None
  789. def _find_model_from_manufacturer_data(
  790. _mfr_id: int, _mfr_data: bytes | None
  791. ) -> str | bytes | None:
  792. """Find model from manufacturer data."""
  793. if _mfr_id not in MODELS_BY_MANUFACTURER_DATA or _mfr_data is None:
  794. return None
  795. for model_chr, model_data in MODELS_BY_MANUFACTURER_DATA[_mfr_id]:
  796. expected_length = model_data.get("manufacturer_data_length")
  797. if expected_length is not None and expected_length == len(_mfr_data):
  798. return model_chr
  799. return None
  800. def _find_model_from_service_data_suffix(_service_data: bytes) -> str | bytes | None:
  801. """Find model from service data suffix."""
  802. if len(_service_data) <= 5:
  803. return None
  804. for s in (_service_data[-4:], _service_data[-5:-1]):
  805. if s in SUPPORTED_TYPES:
  806. return s
  807. return None
  808. def build_advertisement_data(
  809. _model: str | bytes, _service_data: bytes | None, _mfr_data: bytes | None
  810. ) -> dict[str, Any]:
  811. """Build advertisement data dictionary."""
  812. _isEncrypted = bool(_service_data[0] & 0b10000000) if _service_data else False
  813. data = {
  814. "rawAdvData": _service_data,
  815. "data": {},
  816. "model": _model,
  817. "isEncrypted": _isEncrypted,
  818. }
  819. type_data = SUPPORTED_TYPES.get(_model)
  820. if type_data:
  821. model_data = type_data["func"](_service_data, _mfr_data)
  822. if model_data:
  823. data.update(
  824. {
  825. "modelFriendlyName": type_data["modelFriendlyName"],
  826. "modelName": type_data["modelName"],
  827. "data": model_data,
  828. }
  829. )
  830. return data
  831. @lru_cache(maxsize=128)
  832. def _parse_data(
  833. _service_data: bytes | None,
  834. _mfr_data: bytes | None,
  835. _mfr_id: int | None = None,
  836. _switchbot_model: SwitchbotModel | None = None,
  837. ) -> dict[str, Any] | None:
  838. """Parse advertisement data."""
  839. _model = None
  840. if _service_data:
  841. _model = _find_model_from_service_data(_service_data)
  842. if not _model and _switchbot_model:
  843. _model = _find_model_from_switchbot_model(_switchbot_model)
  844. if not _model and _mfr_id:
  845. _model = _find_model_from_manufacturer_data(_mfr_id, _mfr_data)
  846. if not _model and _service_data:
  847. _model = _find_model_from_service_data_suffix(_service_data)
  848. if not _model:
  849. return None
  850. return build_advertisement_data(_model, _service_data, _mfr_data)
  851. def populate_model_to_mac_cache(mac: str, model: SwitchbotModel) -> None:
  852. """Populate the model to MAC address cache."""
  853. _MODEL_TO_MAC_CACHE[mac] = model