test_adv_parser.py 35 KB

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