test_adv_parser.py 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120
  1. from typing import Any
  2. from bleak.backends.device import BLEDevice
  3. from bleak.backends.scanner import AdvertisementData
  4. from switchbot import SwitchbotModel
  5. from switchbot.adv_parser import parse_advertisement_data
  6. from switchbot.models import SwitchBotAdvertisement
  7. ADVERTISEMENT_DATA_DEFAULTS = {
  8. "local_name": "",
  9. "manufacturer_data": {},
  10. "service_data": {},
  11. "service_uuids": [],
  12. "rssi": -127,
  13. "platform_data": ((),),
  14. "tx_power": -127,
  15. }
  16. def generate_advertisement_data(**kwargs: Any) -> AdvertisementData:
  17. """Generate advertisement data with defaults."""
  18. new = kwargs.copy()
  19. for key, value in ADVERTISEMENT_DATA_DEFAULTS.items():
  20. new.setdefault(key, value)
  21. return AdvertisementData(**new)
  22. def test_parse_advertisement_data_curtain():
  23. """Test parse_advertisement_data for curtain."""
  24. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  25. adv_data = generate_advertisement_data(
  26. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  27. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0X\x00\x11\x04"},
  28. rssi=-80,
  29. )
  30. result = parse_advertisement_data(ble_device, adv_data)
  31. assert result == SwitchBotAdvertisement(
  32. address="aa:bb:cc:dd:ee:ff",
  33. data={
  34. "rawAdvData": b"c\xc0X\x00\x11\x04",
  35. "data": {
  36. "calibration": True,
  37. "battery": 88,
  38. "inMotion": False,
  39. "position": 100,
  40. "lightLevel": 1,
  41. "deviceChain": 1,
  42. },
  43. "isEncrypted": False,
  44. "model": "c",
  45. "modelFriendlyName": "Curtain",
  46. "modelName": SwitchbotModel.CURTAIN,
  47. },
  48. device=ble_device,
  49. rssi=-80,
  50. active=True,
  51. )
  52. def test_parse_advertisement_data_curtain_passive():
  53. """Test parse_advertisement_data for curtain passive."""
  54. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  55. adv_data = generate_advertisement_data(
  56. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  57. service_data={},
  58. rssi=-80,
  59. )
  60. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.CURTAIN)
  61. assert result == SwitchBotAdvertisement(
  62. address="aa:bb:cc:dd:ee:ff",
  63. data={
  64. "rawAdvData": None,
  65. "data": {
  66. "calibration": None,
  67. "battery": None,
  68. "inMotion": False,
  69. "position": 100,
  70. "lightLevel": 1,
  71. "deviceChain": 1,
  72. },
  73. "isEncrypted": False,
  74. "model": "c",
  75. "modelFriendlyName": "Curtain",
  76. "modelName": SwitchbotModel.CURTAIN,
  77. },
  78. device=ble_device,
  79. rssi=-80,
  80. active=False,
  81. )
  82. def test_parse_advertisement_data_curtain_position_zero():
  83. """Test parse_advertisement_data for curtain position zero."""
  84. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  85. adv_data = generate_advertisement_data(
  86. local_name="WoCurtain",
  87. manufacturer_data={89: b"\xc1\xc7'}U\xab"},
  88. service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0\xced\x11\x04"},
  89. service_uuids=[
  90. "00001800-0000-1000-8000-00805f9b34fb",
  91. "00001801-0000-1000-8000-00805f9b34fb",
  92. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  93. ],
  94. rssi=-52,
  95. )
  96. result = parse_advertisement_data(ble_device, adv_data)
  97. assert result == SwitchBotAdvertisement(
  98. address="aa:bb:cc:dd:ee:ff",
  99. data={
  100. "rawAdvData": b"c\xd0\xced\x11\x04",
  101. "data": {
  102. "calibration": True,
  103. "battery": 78,
  104. "inMotion": False,
  105. "position": 0,
  106. "lightLevel": 1,
  107. "deviceChain": 1,
  108. },
  109. "isEncrypted": False,
  110. "model": "c",
  111. "modelFriendlyName": "Curtain",
  112. "modelName": SwitchbotModel.CURTAIN,
  113. },
  114. device=ble_device,
  115. rssi=-52,
  116. active=True,
  117. )
  118. def test_parse_advertisement_data_curtain_firmware_six_position_100():
  119. """Test parse_advertisement_data with firmware six for curtain position 100."""
  120. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  121. adv_data = generate_advertisement_data(
  122. local_name="WoCurtain",
  123. manufacturer_data={
  124. 89: b"\xf5\x98\x94\x08\xa0\xe7",
  125. 2409: b'\xf5\x98\x94\x08\xa0\xe7\x9b\x0f\x00"\x04',
  126. },
  127. service_data={
  128. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0H\x00\x12\x04",
  129. "0000fd3d-0000-1000-8000-00805f9b34fb": b'c\xc0G\x00"\x04',
  130. },
  131. service_uuids=[
  132. "00001800-0000-1000-8000-00805f9b34fb",
  133. "00001801-0000-1000-8000-00805f9b34fb",
  134. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  135. ],
  136. rssi=-62,
  137. )
  138. result = parse_advertisement_data(ble_device, adv_data)
  139. assert result == SwitchBotAdvertisement(
  140. address="aa:bb:cc:dd:ee:ff",
  141. data={
  142. "rawAdvData": b'c\xc0G\x00"\x04',
  143. "data": {
  144. "calibration": True,
  145. "battery": 71,
  146. "inMotion": False,
  147. "position": 100,
  148. "lightLevel": 2,
  149. "deviceChain": 2,
  150. },
  151. "isEncrypted": False,
  152. "model": "c",
  153. "modelFriendlyName": "Curtain",
  154. "modelName": SwitchbotModel.CURTAIN,
  155. },
  156. device=ble_device,
  157. rssi=-62,
  158. active=True,
  159. )
  160. def test_parse_advertisement_data_curtain_firmware_six_position_100_other_rssi():
  161. """Test parse_advertisement_data with firmware six for curtain position 100 other rssi."""
  162. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  163. adv_data = generate_advertisement_data(
  164. local_name="WoCurtain",
  165. manufacturer_data={
  166. 89: b"\xf5\x98\x94\x08\xa0\xe7",
  167. 2409: b'\xf5\x98\x94\x08\xa0\xe7\xa5\x0fc"\x04',
  168. },
  169. service_data={
  170. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0H\x00\x12\x04",
  171. "0000fd3d-0000-1000-8000-00805f9b34fb": b'c\xc0Gc"\x04',
  172. },
  173. service_uuids=[
  174. "00001800-0000-1000-8000-00805f9b34fb",
  175. "00001801-0000-1000-8000-00805f9b34fb",
  176. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  177. ],
  178. rssi=-67,
  179. )
  180. result = parse_advertisement_data(ble_device, adv_data)
  181. assert result == SwitchBotAdvertisement(
  182. address="aa:bb:cc:dd:ee:ff",
  183. data={
  184. "rawAdvData": b'c\xc0Gc"\x04',
  185. "data": {
  186. "calibration": True,
  187. "battery": 71,
  188. "inMotion": False,
  189. "position": 1,
  190. "lightLevel": 2,
  191. "deviceChain": 2,
  192. },
  193. "isEncrypted": False,
  194. "model": "c",
  195. "modelFriendlyName": "Curtain",
  196. "modelName": SwitchbotModel.CURTAIN,
  197. },
  198. device=ble_device,
  199. rssi=-67,
  200. active=True,
  201. )
  202. def test_parse_advertisement_data_curtain_fully_closed():
  203. """Test parse_advertisement_data with firmware six fully closed."""
  204. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  205. adv_data = generate_advertisement_data(
  206. local_name="WoCurtain",
  207. manufacturer_data={2409: b"\xc1\xc7'}U\xab\"\x0fd\x11\x04"},
  208. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0Sd\x11\x04"},
  209. service_uuids=[
  210. "00001800-0000-1000-8000-00805f9b34fb",
  211. "00001801-0000-1000-8000-00805f9b34fb",
  212. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  213. ],
  214. rssi=1,
  215. )
  216. result = parse_advertisement_data(ble_device, adv_data)
  217. assert result == SwitchBotAdvertisement(
  218. address="aa:bb:cc:dd:ee:ff",
  219. data={
  220. "rawAdvData": b"c\xc0Sd\x11\x04",
  221. "data": {
  222. "calibration": True,
  223. "battery": 83,
  224. "inMotion": False,
  225. "position": 0,
  226. "lightLevel": 1,
  227. "deviceChain": 1,
  228. },
  229. "isEncrypted": False,
  230. "model": "c",
  231. "modelFriendlyName": "Curtain",
  232. "modelName": SwitchbotModel.CURTAIN,
  233. },
  234. device=ble_device,
  235. rssi=1,
  236. active=True,
  237. )
  238. def test_parse_advertisement_data_curtain_fully_open():
  239. """Test parse_advertisement_data with firmware six fully open."""
  240. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  241. adv_data = generate_advertisement_data(
  242. local_name="WoCurtain",
  243. manufacturer_data={2409: b"\xc1\xc7'}U\xab%\x0f\x00\x11\x04"},
  244. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0S\x00\x11\x04"},
  245. service_uuids=[
  246. "00001800-0000-1000-8000-00805f9b34fb",
  247. "00001801-0000-1000-8000-00805f9b34fb",
  248. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  249. ],
  250. rssi=1,
  251. )
  252. result = parse_advertisement_data(ble_device, adv_data)
  253. assert result == SwitchBotAdvertisement(
  254. address="aa:bb:cc:dd:ee:ff",
  255. data={
  256. "rawAdvData": b"c\xc0S\x00\x11\x04",
  257. "data": {
  258. "calibration": True,
  259. "battery": 83,
  260. "inMotion": False,
  261. "position": 100,
  262. "lightLevel": 1,
  263. "deviceChain": 1,
  264. },
  265. "isEncrypted": False,
  266. "model": "c",
  267. "modelFriendlyName": "Curtain",
  268. "modelName": SwitchbotModel.CURTAIN,
  269. },
  270. device=ble_device,
  271. rssi=1,
  272. active=True,
  273. )
  274. def test_parse_advertisement_data_contact():
  275. """Test parse_advertisement_data for the contact sensor."""
  276. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  277. adv_data = generate_advertisement_data(
  278. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  279. service_data={
  280. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@d\x05\x00u\x00\xf8\x12"
  281. },
  282. rssi=-80,
  283. )
  284. result = parse_advertisement_data(ble_device, adv_data)
  285. assert result == SwitchBotAdvertisement(
  286. address="aa:bb:cc:dd:ee:ff",
  287. data={
  288. "rawAdvData": b"d@d\x05\x00u\x00\xf8\x12",
  289. "data": {
  290. "button_count": 2,
  291. "contact_open": True,
  292. "contact_timeout": True,
  293. "is_light": True,
  294. "battery": 100,
  295. "motion_detected": True,
  296. "tested": False,
  297. },
  298. "isEncrypted": False,
  299. "model": "d",
  300. "modelFriendlyName": "Contact Sensor",
  301. "modelName": SwitchbotModel.CONTACT_SENSOR,
  302. },
  303. device=ble_device,
  304. rssi=-80,
  305. active=True,
  306. )
  307. def test_parse_advertisement_data_empty():
  308. """Test parse_advertisement_data with empty data does not blow up."""
  309. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  310. adv_data = generate_advertisement_data(
  311. manufacturer_data={2403: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  312. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b""},
  313. )
  314. result = parse_advertisement_data(ble_device, adv_data)
  315. assert result is None
  316. def test_new_bot_firmware():
  317. """Test parsing adv data from new bot firmware."""
  318. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  319. adv_data = generate_advertisement_data(
  320. manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
  321. service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10\xe1"},
  322. service_uuids=["CBA20D00-224D-11E6-9FB8-0002A5D5C51B"],
  323. rssi=-90,
  324. )
  325. result = parse_advertisement_data(ble_device, adv_data)
  326. assert result == SwitchBotAdvertisement(
  327. address="aa:bb:cc:dd:ee:ff",
  328. data={
  329. "rawAdvData": b"H\x10\xe1",
  330. "data": {"switchMode": False, "isOn": False, "battery": 97},
  331. "model": "H",
  332. "isEncrypted": False,
  333. "modelFriendlyName": "Bot",
  334. "modelName": SwitchbotModel.BOT,
  335. },
  336. device=ble_device,
  337. rssi=-90,
  338. active=True,
  339. )
  340. def test_parse_advertisement_data_curtain_firmware_six_fully_closed():
  341. """Test parse_advertisement_data with firmware six fully closed."""
  342. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  343. adv_data = generate_advertisement_data(
  344. local_name="WoCurtain",
  345. manufacturer_data={
  346. 89: b"\xcc\xf4\xc4\xf9\xacl",
  347. 2409: b"\xcc\xf4\xc4\xf9\xacl\xeb\x0fd\x12\x04",
  348. },
  349. service_data={
  350. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Yd\x11\x04",
  351. "0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0dd\x12\x04",
  352. },
  353. service_uuids=[
  354. "00001800-0000-1000-8000-00805f9b34fb",
  355. "00001801-0000-1000-8000-00805f9b34fb",
  356. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  357. ],
  358. rssi=-2,
  359. )
  360. result = parse_advertisement_data(ble_device, adv_data)
  361. assert result == SwitchBotAdvertisement(
  362. address="aa:bb:cc:dd:ee:ff",
  363. data={
  364. "rawAdvData": b"c\xc0dd\x12\x04",
  365. "data": {
  366. "calibration": True,
  367. "battery": 100,
  368. "inMotion": False,
  369. "position": 0,
  370. "lightLevel": 1,
  371. "deviceChain": 2,
  372. },
  373. "isEncrypted": False,
  374. "model": "c",
  375. "modelFriendlyName": "Curtain",
  376. "modelName": SwitchbotModel.CURTAIN,
  377. },
  378. device=ble_device,
  379. rssi=-2,
  380. active=True,
  381. )
  382. def test_parse_advertisement_data_curtain_firmware_six_fully_open():
  383. """Test parse_advertisement_data with firmware six fully open."""
  384. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  385. adv_data = generate_advertisement_data(
  386. local_name="WoCurtain",
  387. manufacturer_data={
  388. 89: b"\xcc\xf4\xc4\xf9\xacl",
  389. 2409: b"\xcc\xf4\xc4\xf9\xacl\xe2\x0f\x00\x12\x04",
  390. },
  391. service_data={
  392. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Yd\x11\x04",
  393. "0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0d\x00\x12\x04",
  394. },
  395. service_uuids=[
  396. "00001800-0000-1000-8000-00805f9b34fb",
  397. "00001801-0000-1000-8000-00805f9b34fb",
  398. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  399. ],
  400. rssi=-2,
  401. )
  402. result = parse_advertisement_data(ble_device, adv_data)
  403. assert result == SwitchBotAdvertisement(
  404. address="aa:bb:cc:dd:ee:ff",
  405. data={
  406. "rawAdvData": b"c\xc0d\x00\x12\x04",
  407. "data": {
  408. "calibration": True,
  409. "battery": 100,
  410. "inMotion": False,
  411. "position": 100,
  412. "lightLevel": 1,
  413. "deviceChain": 2,
  414. },
  415. "isEncrypted": False,
  416. "model": "c",
  417. "modelFriendlyName": "Curtain",
  418. "modelName": SwitchbotModel.CURTAIN,
  419. },
  420. device=ble_device,
  421. rssi=-2,
  422. active=True,
  423. )
  424. def test_contact_sensor_mfr():
  425. """Test parsing adv data from new bot firmware."""
  426. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  427. adv_data = generate_advertisement_data(
  428. manufacturer_data={2409: b"\xcb9\xcd\xc4=FA,\x00F\x01\x8f\xc4"},
  429. service_data={
  430. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d\x00\xda\x04\x00F\x01\x8f\xc4"
  431. },
  432. tx_power=-127,
  433. rssi=-70,
  434. )
  435. result = parse_advertisement_data(ble_device, adv_data)
  436. assert result == SwitchBotAdvertisement(
  437. address="aa:bb:cc:dd:ee:ff",
  438. data={
  439. "data": {
  440. "battery": 90,
  441. "button_count": 4,
  442. "contact_open": True,
  443. "contact_timeout": True,
  444. "is_light": False,
  445. "motion_detected": False,
  446. "tested": False,
  447. },
  448. "isEncrypted": False,
  449. "model": "d",
  450. "modelFriendlyName": "Contact Sensor",
  451. "modelName": SwitchbotModel.CONTACT_SENSOR,
  452. "rawAdvData": b"d\x00\xda\x04\x00F\x01\x8f\xc4",
  453. },
  454. device=ble_device,
  455. rssi=-70,
  456. active=True,
  457. )
  458. def test_contact_sensor_mfr_no_service_data():
  459. """Test contact sensor with passive data only."""
  460. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  461. adv_data = generate_advertisement_data(
  462. manufacturer_data={2409: b"\xcb9\xcd\xc4=FA,\x00F\x01\x8f\xc4"},
  463. service_data={},
  464. tx_power=-127,
  465. rssi=-70,
  466. )
  467. result = parse_advertisement_data(ble_device, adv_data)
  468. assert result == SwitchBotAdvertisement(
  469. address="aa:bb:cc:dd:ee:ff",
  470. data={
  471. "data": {
  472. "battery": None,
  473. "button_count": 4,
  474. "contact_open": False,
  475. "contact_timeout": False,
  476. "is_light": False,
  477. "motion_detected": False,
  478. "tested": None,
  479. },
  480. "isEncrypted": False,
  481. "model": "d",
  482. "modelFriendlyName": "Contact Sensor",
  483. "modelName": SwitchbotModel.CONTACT_SENSOR,
  484. "rawAdvData": None,
  485. },
  486. device=ble_device,
  487. rssi=-70,
  488. active=False,
  489. )
  490. def test_contact_sensor_srv():
  491. """Test parsing adv data from new bot firmware."""
  492. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  493. adv_data = generate_advertisement_data(
  494. service_data={
  495. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d\x00\xda\x04\x00F\x01\x8f\xc4"
  496. },
  497. tx_power=-127,
  498. rssi=-70,
  499. )
  500. result = parse_advertisement_data(ble_device, adv_data)
  501. assert result == SwitchBotAdvertisement(
  502. address="aa:bb:cc:dd:ee:ff",
  503. data={
  504. "data": {
  505. "battery": 90,
  506. "button_count": 4,
  507. "contact_open": True,
  508. "contact_timeout": True,
  509. "is_light": False,
  510. "motion_detected": False,
  511. "tested": False,
  512. },
  513. "isEncrypted": False,
  514. "model": "d",
  515. "modelFriendlyName": "Contact Sensor",
  516. "modelName": SwitchbotModel.CONTACT_SENSOR,
  517. "rawAdvData": b"d\x00\xda\x04\x00F\x01\x8f\xc4",
  518. },
  519. device=ble_device,
  520. rssi=-70,
  521. active=True,
  522. )
  523. def test_contact_sensor_open():
  524. """Test parsing mfr adv data from new bot firmware."""
  525. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  526. adv_data = generate_advertisement_data(
  527. manufacturer_data={2409: b"\xcb9\xcd\xc4=F\x84\x9c\x00\x17\x00QD"},
  528. service_data={
  529. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@\xda\x02\x00\x17\x00QD"
  530. },
  531. tx_power=-127,
  532. rssi=-59,
  533. )
  534. result = parse_advertisement_data(ble_device, adv_data)
  535. assert result == SwitchBotAdvertisement(
  536. address="aa:bb:cc:dd:ee:ff",
  537. data={
  538. "data": {
  539. "battery": 90,
  540. "button_count": 4,
  541. "contact_open": True,
  542. "contact_timeout": False,
  543. "is_light": False,
  544. "motion_detected": True,
  545. "tested": False,
  546. },
  547. "isEncrypted": False,
  548. "model": "d",
  549. "modelFriendlyName": "Contact Sensor",
  550. "modelName": SwitchbotModel.CONTACT_SENSOR,
  551. "rawAdvData": b"d@\xda\x02\x00\x17\x00QD",
  552. },
  553. device=ble_device,
  554. rssi=-59,
  555. active=True,
  556. )
  557. def test_contact_sensor_closed():
  558. """Test parsing mfr adv data from new bot firmware."""
  559. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  560. adv_data = generate_advertisement_data(
  561. manufacturer_data={2409: b"\xcb9\xcd\xc4=F\x89\x8c\x00+\x00\x19\x84"},
  562. service_data={
  563. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@\xda\x00\x00+\x00\x19\x84"
  564. },
  565. tx_power=-127,
  566. rssi=-50,
  567. )
  568. result = parse_advertisement_data(ble_device, adv_data)
  569. assert result == SwitchBotAdvertisement(
  570. address="aa:bb:cc:dd:ee:ff",
  571. data={
  572. "data": {
  573. "battery": 90,
  574. "button_count": 4,
  575. "contact_open": False,
  576. "contact_timeout": False,
  577. "is_light": False,
  578. "motion_detected": True,
  579. "tested": False,
  580. },
  581. "isEncrypted": False,
  582. "model": "d",
  583. "modelFriendlyName": "Contact Sensor",
  584. "modelName": SwitchbotModel.CONTACT_SENSOR,
  585. "rawAdvData": b"d@\xda\x00\x00+\x00\x19\x84",
  586. },
  587. device=ble_device,
  588. rssi=-50,
  589. active=True,
  590. )
  591. def test_switchbot_passive():
  592. """Test parsing switchbot as passive."""
  593. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  594. adv_data = generate_advertisement_data(
  595. manufacturer_data={89: bytes.fromhex("d51cfb397856")},
  596. service_data={},
  597. tx_power=-127,
  598. rssi=-50,
  599. )
  600. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.BOT)
  601. assert result == SwitchBotAdvertisement(
  602. address="aa:bb:cc:dd:ee:ff",
  603. data={
  604. "data": {
  605. "battery": None,
  606. "switchMode": None,
  607. "isOn": None,
  608. },
  609. "isEncrypted": False,
  610. "model": "H",
  611. "modelFriendlyName": "Bot",
  612. "modelName": SwitchbotModel.BOT,
  613. "rawAdvData": None,
  614. },
  615. device=ble_device,
  616. rssi=-50,
  617. active=False,
  618. )
  619. def test_bulb_active():
  620. """Test parsing bulb as active."""
  621. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  622. adv_data = generate_advertisement_data(
  623. manufacturer_data={2409: b"\x84\xf7\x03\xb4\xcbz\x03\xe4!\x00\x00"},
  624. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"u\x00d"},
  625. tx_power=-127,
  626. rssi=-50,
  627. )
  628. result = parse_advertisement_data(ble_device, adv_data)
  629. assert result == SwitchBotAdvertisement(
  630. address="aa:bb:cc:dd:ee:ff",
  631. data={
  632. "data": {
  633. "brightness": 100,
  634. "color_mode": 1,
  635. "delay": False,
  636. "isOn": True,
  637. "loop_index": 0,
  638. "preset": False,
  639. "sequence_number": 3,
  640. "speed": 0,
  641. },
  642. "isEncrypted": False,
  643. "model": "u",
  644. "modelFriendlyName": "Color Bulb",
  645. "modelName": SwitchbotModel.COLOR_BULB,
  646. "rawAdvData": b"u\x00d",
  647. },
  648. device=ble_device,
  649. rssi=-50,
  650. active=True,
  651. )
  652. def test_lightstrip_passive():
  653. """Test parsing lightstrip as passive."""
  654. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  655. adv_data = generate_advertisement_data(
  656. manufacturer_data={
  657. 2409: b"`U\xf9(\xe5\x96\x00d\x02\xb0\x00\x00\x00\x00\x00\x00"
  658. },
  659. service_data={},
  660. tx_power=-127,
  661. rssi=-50,
  662. )
  663. result = parse_advertisement_data(ble_device, adv_data)
  664. assert result == SwitchBotAdvertisement(
  665. address="aa:bb:cc:dd:ee:ff",
  666. data={
  667. "data": {
  668. "brightness": 100,
  669. "color_mode": 2,
  670. "delay": False,
  671. "isOn": False,
  672. "loop_index": 0,
  673. "preset": False,
  674. "sequence_number": 0,
  675. "speed": 48,
  676. },
  677. "isEncrypted": False,
  678. "model": "r",
  679. "modelFriendlyName": "Light Strip",
  680. "modelName": SwitchbotModel.LIGHT_STRIP,
  681. "rawAdvData": None,
  682. },
  683. device=ble_device,
  684. rssi=-50,
  685. active=False,
  686. )
  687. def test_wosensor_passive_and_active():
  688. """Test parsing wosensor as passive with active data as well."""
  689. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  690. adv_data = generate_advertisement_data(
  691. manufacturer_data={2409: b"\xd7\xc1}]\xebC\xde\x03\x06\x985"},
  692. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\xe4\x06\x985"},
  693. tx_power=-127,
  694. rssi=-50,
  695. )
  696. result = parse_advertisement_data(ble_device, adv_data)
  697. assert result == SwitchBotAdvertisement(
  698. address="aa:bb:cc:dd:ee:ff",
  699. data={
  700. "data": {
  701. "battery": 100,
  702. "fahrenheit": False,
  703. "humidity": 53,
  704. "temp": {"c": 24.6, "f": 76.28},
  705. "temperature": 24.6,
  706. },
  707. "isEncrypted": False,
  708. "model": "T",
  709. "modelFriendlyName": "Meter",
  710. "modelName": SwitchbotModel.METER,
  711. "rawAdvData": b"T\x00\xe4\x06\x985",
  712. },
  713. device=ble_device,
  714. rssi=-50,
  715. active=True,
  716. )
  717. def test_wosensor_active():
  718. """Test parsing wosensor with active data as well."""
  719. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  720. adv_data = generate_advertisement_data(
  721. manufacturer_data={},
  722. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\xe4\x06\x985"},
  723. tx_power=-127,
  724. rssi=-50,
  725. )
  726. result = parse_advertisement_data(ble_device, adv_data)
  727. assert result == SwitchBotAdvertisement(
  728. address="aa:bb:cc:dd:ee:ff",
  729. data={
  730. "data": {
  731. "battery": 100,
  732. "fahrenheit": False,
  733. "humidity": 53,
  734. "temp": {"c": 24.6, "f": 76.28},
  735. "temperature": 24.6,
  736. },
  737. "isEncrypted": False,
  738. "model": "T",
  739. "modelFriendlyName": "Meter",
  740. "modelName": SwitchbotModel.METER,
  741. "rawAdvData": b"T\x00\xe4\x06\x985",
  742. },
  743. device=ble_device,
  744. rssi=-50,
  745. active=True,
  746. )
  747. def test_wosensor_passive_only():
  748. """Test parsing wosensor with only passive data."""
  749. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  750. adv_data = generate_advertisement_data(
  751. manufacturer_data={2409: b"\xd7\xc1}]\xebC\xde\x03\x06\x985"},
  752. service_data={},
  753. tx_power=-127,
  754. rssi=-50,
  755. )
  756. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.METER)
  757. assert result == SwitchBotAdvertisement(
  758. address="aa:bb:cc:dd:ee:ff",
  759. data={
  760. "data": {
  761. "battery": None,
  762. "fahrenheit": False,
  763. "humidity": 53,
  764. "temp": {"c": 24.6, "f": 76.28},
  765. "temperature": 24.6,
  766. },
  767. "isEncrypted": False,
  768. "model": "T",
  769. "modelFriendlyName": "Meter",
  770. "modelName": SwitchbotModel.METER,
  771. "rawAdvData": None,
  772. },
  773. device=ble_device,
  774. rssi=-50,
  775. active=False,
  776. )
  777. def test_motion_sensor_clear():
  778. """Test parsing motion sensor with clear data."""
  779. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  780. adv_data = generate_advertisement_data(
  781. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
  782. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x00f\x01"},
  783. tx_power=-127,
  784. rssi=-87,
  785. )
  786. result = parse_advertisement_data(
  787. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  788. )
  789. assert result == SwitchBotAdvertisement(
  790. address="aa:bb:cc:dd:ee:ff",
  791. data={
  792. "data": {
  793. "battery": 98,
  794. "iot": 0,
  795. "is_light": False,
  796. "led": 0,
  797. "light_intensity": 1,
  798. "motion_detected": False,
  799. "sense_distance": 0,
  800. "tested": False,
  801. },
  802. "isEncrypted": False,
  803. "model": "s",
  804. "modelFriendlyName": "Motion Sensor",
  805. "modelName": SwitchbotModel.MOTION_SENSOR,
  806. "rawAdvData": b"s\x00\xe2\x00f\x01",
  807. },
  808. device=ble_device,
  809. rssi=-87,
  810. active=True,
  811. )
  812. def test_motion_sensor_clear_passive():
  813. """Test parsing motion sensor with clear data."""
  814. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  815. adv_data = generate_advertisement_data(
  816. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
  817. service_data={},
  818. tx_power=-127,
  819. rssi=-87,
  820. )
  821. result = parse_advertisement_data(
  822. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  823. )
  824. assert result == SwitchBotAdvertisement(
  825. address="aa:bb:cc:dd:ee:ff",
  826. data={
  827. "data": {
  828. "battery": None,
  829. "iot": None,
  830. "is_light": False,
  831. "led": None,
  832. "light_intensity": None,
  833. "motion_detected": False,
  834. "sense_distance": None,
  835. "tested": None,
  836. },
  837. "isEncrypted": False,
  838. "model": "s",
  839. "modelFriendlyName": "Motion Sensor",
  840. "modelName": SwitchbotModel.MOTION_SENSOR,
  841. "rawAdvData": None,
  842. },
  843. device=ble_device,
  844. rssi=-87,
  845. active=False,
  846. )
  847. def test_motion_sensor_motion():
  848. """Test parsing motion sensor with motion data."""
  849. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  850. adv_data = generate_advertisement_data(
  851. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
  852. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x008\x01"},
  853. tx_power=-127,
  854. rssi=-87,
  855. )
  856. result = parse_advertisement_data(
  857. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  858. )
  859. assert result == SwitchBotAdvertisement(
  860. address="aa:bb:cc:dd:ee:ff",
  861. data={
  862. "data": {
  863. "battery": 98,
  864. "iot": 0,
  865. "is_light": False,
  866. "led": 0,
  867. "light_intensity": 1,
  868. "motion_detected": True,
  869. "sense_distance": 0,
  870. "tested": False,
  871. },
  872. "isEncrypted": False,
  873. "model": "s",
  874. "modelFriendlyName": "Motion Sensor",
  875. "modelName": SwitchbotModel.MOTION_SENSOR,
  876. "rawAdvData": b"s@\xe2\x008\x01",
  877. },
  878. device=ble_device,
  879. rssi=-87,
  880. active=True,
  881. )
  882. def test_motion_sensor_motion_passive():
  883. """Test parsing motion sensor with motion data."""
  884. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  885. adv_data = generate_advertisement_data(
  886. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
  887. service_data={},
  888. tx_power=-127,
  889. rssi=-87,
  890. )
  891. result = parse_advertisement_data(
  892. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  893. )
  894. assert result == SwitchBotAdvertisement(
  895. address="aa:bb:cc:dd:ee:ff",
  896. data={
  897. "data": {
  898. "battery": None,
  899. "iot": None,
  900. "is_light": False,
  901. "led": None,
  902. "light_intensity": None,
  903. "motion_detected": True,
  904. "sense_distance": None,
  905. "tested": None,
  906. },
  907. "isEncrypted": False,
  908. "model": "s",
  909. "modelFriendlyName": "Motion Sensor",
  910. "modelName": SwitchbotModel.MOTION_SENSOR,
  911. "rawAdvData": None,
  912. },
  913. device=ble_device,
  914. rssi=-87,
  915. active=False,
  916. )
  917. def test_motion_sensor_is_light_passive():
  918. """Test parsing motion sensor with motion data."""
  919. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  920. adv_data = generate_advertisement_data(
  921. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIs,\x04g"},
  922. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
  923. tx_power=-127,
  924. rssi=-93,
  925. )
  926. result = parse_advertisement_data(
  927. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  928. )
  929. assert result == SwitchBotAdvertisement(
  930. address="aa:bb:cc:dd:ee:ff",
  931. data={
  932. "data": {
  933. "battery": 98,
  934. "iot": 0,
  935. "is_light": True,
  936. "led": 0,
  937. "light_intensity": 2,
  938. "motion_detected": False,
  939. "sense_distance": 0,
  940. "tested": False,
  941. },
  942. "isEncrypted": False,
  943. "model": "s",
  944. "modelFriendlyName": "Motion Sensor",
  945. "modelName": SwitchbotModel.MOTION_SENSOR,
  946. "rawAdvData": b"s\x00\xe2\x04g\x02",
  947. },
  948. device=ble_device,
  949. rssi=-93,
  950. active=True,
  951. )
  952. def test_motion_sensor_is_light_active():
  953. """Test parsing motion sensor with motion data."""
  954. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  955. adv_data = generate_advertisement_data(
  956. manufacturer_data={},
  957. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
  958. tx_power=-127,
  959. rssi=-93,
  960. )
  961. result = parse_advertisement_data(
  962. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  963. )
  964. assert result == SwitchBotAdvertisement(
  965. address="aa:bb:cc:dd:ee:ff",
  966. data={
  967. "data": {
  968. "battery": 98,
  969. "iot": 0,
  970. "is_light": True,
  971. "led": 0,
  972. "light_intensity": 2,
  973. "motion_detected": False,
  974. "sense_distance": 0,
  975. "tested": False,
  976. },
  977. "isEncrypted": False,
  978. "model": "s",
  979. "modelFriendlyName": "Motion Sensor",
  980. "modelName": SwitchbotModel.MOTION_SENSOR,
  981. "rawAdvData": b"s\x00\xe2\x04g\x02",
  982. },
  983. device=ble_device,
  984. rssi=-93,
  985. active=True,
  986. )
  987. def test_motion_with_light_detected():
  988. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  989. adv_data = generate_advertisement_data(
  990. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIvl\x00,"},
  991. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x00,\x02"},
  992. tx_power=-127,
  993. rssi=-84,
  994. )
  995. result = parse_advertisement_data(
  996. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  997. )
  998. assert result == SwitchBotAdvertisement(
  999. address="aa:bb:cc:dd:ee:ff",
  1000. data={
  1001. "data": {
  1002. "battery": 98,
  1003. "iot": 0,
  1004. "is_light": True,
  1005. "led": 0,
  1006. "light_intensity": 2,
  1007. "motion_detected": True,
  1008. "sense_distance": 0,
  1009. "tested": False,
  1010. },
  1011. "isEncrypted": False,
  1012. "model": "s",
  1013. "modelFriendlyName": "Motion Sensor",
  1014. "modelName": SwitchbotModel.MOTION_SENSOR,
  1015. "rawAdvData": b"s@\xe2\x00,\x02",
  1016. },
  1017. device=ble_device,
  1018. rssi=-84,
  1019. active=True,
  1020. )
  1021. def test_motion_sensor_motion_passive():
  1022. """Test parsing motion sensor with motion data."""
  1023. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  1024. adv_data = generate_advertisement_data(
  1025. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
  1026. service_data={},
  1027. tx_power=-127,
  1028. rssi=-87,
  1029. )
  1030. result = parse_advertisement_data(ble_device, adv_data)
  1031. assert result == SwitchBotAdvertisement(
  1032. address="aa:bb:cc:dd:ee:ff",
  1033. data={
  1034. "data": {
  1035. "battery": None,
  1036. "iot": None,
  1037. "is_light": False,
  1038. "led": None,
  1039. "light_intensity": None,
  1040. "motion_detected": True,
  1041. "sense_distance": None,
  1042. "tested": None,
  1043. },
  1044. "isEncrypted": False,
  1045. "model": "s",
  1046. "modelFriendlyName": "Motion Sensor",
  1047. "modelName": SwitchbotModel.MOTION_SENSOR,
  1048. "rawAdvData": None,
  1049. },
  1050. device=ble_device,
  1051. rssi=-87,
  1052. active=False,
  1053. )