adv_parser.py 29 KB

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