test_adv_parser.py 39 KB

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