test_adv_parser.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. from typing import Any
  2. from bleak.backends.device import BLEDevice
  3. from bleak.backends.scanner import AdvertisementData
  4. from switchbot import LockStatus, 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(
  1031. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1032. )
  1033. assert result == SwitchBotAdvertisement(
  1034. address="aa:bb:cc:dd:ee:ff",
  1035. data={
  1036. "data": {
  1037. "battery": None,
  1038. "iot": None,
  1039. "is_light": False,
  1040. "led": None,
  1041. "light_intensity": None,
  1042. "motion_detected": True,
  1043. "sense_distance": None,
  1044. "tested": None,
  1045. },
  1046. "isEncrypted": False,
  1047. "model": "s",
  1048. "modelFriendlyName": "Motion Sensor",
  1049. "modelName": SwitchbotModel.MOTION_SENSOR,
  1050. "rawAdvData": None,
  1051. },
  1052. device=ble_device,
  1053. rssi=-87,
  1054. active=False,
  1055. )
  1056. def test_parsing_lock_active():
  1057. """Test parsing lock with active data."""
  1058. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  1059. adv_data = generate_advertisement_data(
  1060. manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\x07\x83\x00 "},
  1061. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"o\x80d"},
  1062. rssi=-67,
  1063. )
  1064. result = parse_advertisement_data(ble_device, adv_data)
  1065. assert result == SwitchBotAdvertisement(
  1066. address="aa:bb:cc:dd:ee:ff",
  1067. data={
  1068. "data": {
  1069. "auto_lock_paused": False,
  1070. "battery": 100,
  1071. "calibration": True,
  1072. "door_open": False,
  1073. "double_lock_mode": False,
  1074. "status": LockStatus.LOCKED,
  1075. "unclosed_alarm": False,
  1076. "unlocked_alarm": False,
  1077. "update_from_secondary_lock": False,
  1078. },
  1079. "isEncrypted": False,
  1080. "model": "o",
  1081. "modelFriendlyName": "Lock",
  1082. "modelName": SwitchbotModel.LOCK,
  1083. "rawAdvData": b"o\x80d",
  1084. },
  1085. device=ble_device,
  1086. rssi=-67,
  1087. active=True,
  1088. )
  1089. def test_parsing_lock_passive():
  1090. """Test parsing lock with active data."""
  1091. ble_device = BLEDevice("aa:bb:cc:dd:ee:ff", "any")
  1092. adv_data = generate_advertisement_data(
  1093. manufacturer_data={2409: b"\xf1\t\x9fE\x1a]\x07\x83\x00 "}, rssi=-67
  1094. )
  1095. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LOCK)
  1096. assert result == SwitchBotAdvertisement(
  1097. address="aa:bb:cc:dd:ee:ff",
  1098. data={
  1099. "data": {
  1100. "auto_lock_paused": False,
  1101. "battery": None,
  1102. "calibration": True,
  1103. "door_open": False,
  1104. "double_lock_mode": False,
  1105. "status": LockStatus.LOCKED,
  1106. "unclosed_alarm": False,
  1107. "unlocked_alarm": False,
  1108. "update_from_secondary_lock": False,
  1109. },
  1110. "isEncrypted": False,
  1111. "model": "o",
  1112. "modelFriendlyName": "Lock",
  1113. "modelName": SwitchbotModel.LOCK,
  1114. "rawAdvData": None,
  1115. },
  1116. device=ble_device,
  1117. rssi=-67,
  1118. active=False,
  1119. )