test_adv_parser.py 165 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141
  1. from __future__ import annotations
  2. import datetime
  3. from typing import Any
  4. import pytest
  5. from bleak.backends.device import BLEDevice
  6. from bleak.backends.scanner import AdvertisementData
  7. from switchbot import HumidifierMode, SwitchbotModel
  8. from switchbot.adv_parser import (
  9. _MODEL_TO_MAC_CACHE,
  10. parse_advertisement_data,
  11. populate_model_to_mac_cache,
  12. )
  13. from switchbot.adv_parsers.fan import process_circulator_fan_pro
  14. from switchbot.const.lock import LockStatus
  15. from switchbot.models import SwitchBotAdvertisement
  16. from . import AdvTestCase
  17. ADVERTISEMENT_DATA_DEFAULTS = {
  18. "local_name": "",
  19. "manufacturer_data": {},
  20. "service_data": {},
  21. "service_uuids": [],
  22. "rssi": -127,
  23. "platform_data": ((),),
  24. "tx_power": -127,
  25. }
  26. BLE_DEVICE_DEFAULTS = {
  27. "name": None,
  28. "rssi": -127,
  29. "details": None,
  30. }
  31. def generate_ble_device(
  32. address: str | None = None,
  33. name: str | None = None,
  34. details: Any | None = None,
  35. rssi: int | None = None,
  36. **kwargs: Any,
  37. ) -> BLEDevice:
  38. """Generate a BLEDevice with defaults."""
  39. new = kwargs.copy()
  40. if address is not None:
  41. new["address"] = address
  42. if name is not None:
  43. new["name"] = name
  44. if details is not None:
  45. new["details"] = details
  46. if rssi is not None:
  47. new["rssi"] = rssi
  48. for key, value in BLE_DEVICE_DEFAULTS.items():
  49. new.setdefault(key, value)
  50. return BLEDevice(**new)
  51. def generate_advertisement_data(**kwargs: Any) -> AdvertisementData:
  52. """Generate advertisement data with defaults."""
  53. new = kwargs.copy()
  54. for key, value in ADVERTISEMENT_DATA_DEFAULTS.items():
  55. new.setdefault(key, value)
  56. return AdvertisementData(**new)
  57. def test_parse_advertisement_data_curtain():
  58. """Test parse_advertisement_data for curtain."""
  59. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  60. adv_data = generate_advertisement_data(
  61. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  62. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0X\x00\x11\x04"},
  63. rssi=-80,
  64. )
  65. result = parse_advertisement_data(ble_device, adv_data)
  66. assert result == SwitchBotAdvertisement(
  67. address="aa:bb:cc:dd:ee:ff",
  68. data={
  69. "rawAdvData": b"c\xc0X\x00\x11\x04",
  70. "data": {
  71. "calibration": True,
  72. "battery": 88,
  73. "inMotion": False,
  74. "position": 100,
  75. "lightLevel": 1,
  76. "deviceChain": 1,
  77. },
  78. "isEncrypted": False,
  79. "model": "c",
  80. "modelFriendlyName": "Curtain",
  81. "modelName": SwitchbotModel.CURTAIN,
  82. },
  83. device=ble_device,
  84. rssi=-80,
  85. active=True,
  86. )
  87. def test_parse_advertisement_data_curtain_passive():
  88. """Test parse_advertisement_data for curtain passive."""
  89. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  90. adv_data = generate_advertisement_data(
  91. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  92. service_data={},
  93. rssi=-80,
  94. )
  95. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.CURTAIN)
  96. assert result == SwitchBotAdvertisement(
  97. address="aa:bb:cc:dd:ee:ff",
  98. data={
  99. "rawAdvData": None,
  100. "data": {
  101. "calibration": None,
  102. "battery": None,
  103. "inMotion": False,
  104. "position": 100,
  105. "lightLevel": 1,
  106. "deviceChain": 1,
  107. },
  108. "isEncrypted": False,
  109. "model": "{",
  110. "modelFriendlyName": "Curtain 3",
  111. "modelName": SwitchbotModel.CURTAIN,
  112. },
  113. device=ble_device,
  114. rssi=-80,
  115. active=False,
  116. )
  117. def test_parse_advertisement_data_curtain_passive_12_bytes():
  118. """Test parse_advertisement_data for curtain passive."""
  119. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  120. adv_data = generate_advertisement_data(
  121. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04\x00"},
  122. service_data={},
  123. rssi=-80,
  124. )
  125. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.CURTAIN)
  126. assert result == SwitchBotAdvertisement(
  127. address="aa:bb:cc:dd:ee:ff",
  128. data={
  129. "rawAdvData": None,
  130. "data": {
  131. "calibration": None,
  132. "battery": None,
  133. "inMotion": False,
  134. "position": 100,
  135. "lightLevel": 1,
  136. "deviceChain": 1,
  137. },
  138. "isEncrypted": False,
  139. "model": "{",
  140. "modelFriendlyName": "Curtain 3",
  141. "modelName": SwitchbotModel.CURTAIN,
  142. },
  143. device=ble_device,
  144. rssi=-80,
  145. active=False,
  146. )
  147. def test_parse_advertisement_data_curtain_position_zero():
  148. """Test parse_advertisement_data for curtain position zero."""
  149. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  150. adv_data = generate_advertisement_data(
  151. local_name="WoCurtain",
  152. manufacturer_data={89: b"\xc1\xc7'}U\xab"},
  153. service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0\xced\x11\x04"},
  154. service_uuids=[
  155. "00001800-0000-1000-8000-00805f9b34fb",
  156. "00001801-0000-1000-8000-00805f9b34fb",
  157. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  158. ],
  159. rssi=-52,
  160. )
  161. result = parse_advertisement_data(ble_device, adv_data)
  162. assert result == SwitchBotAdvertisement(
  163. address="aa:bb:cc:dd:ee:ff",
  164. data={
  165. "rawAdvData": b"c\xd0\xced\x11\x04",
  166. "data": {
  167. "calibration": True,
  168. "battery": 78,
  169. "inMotion": False,
  170. "position": 0,
  171. "lightLevel": 1,
  172. "deviceChain": 1,
  173. },
  174. "isEncrypted": False,
  175. "model": "c",
  176. "modelFriendlyName": "Curtain",
  177. "modelName": SwitchbotModel.CURTAIN,
  178. },
  179. device=ble_device,
  180. rssi=-52,
  181. active=True,
  182. )
  183. def test_parse_advertisement_data_curtain_firmware_six_position_100():
  184. """Test parse_advertisement_data with firmware six for curtain position 100."""
  185. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  186. adv_data = generate_advertisement_data(
  187. local_name="WoCurtain",
  188. manufacturer_data={
  189. 89: b"\xf5\x98\x94\x08\xa0\xe7",
  190. 2409: b'\xf5\x98\x94\x08\xa0\xe7\x9b\x0f\x00"\x04',
  191. },
  192. service_data={
  193. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0H\x00\x12\x04",
  194. "0000fd3d-0000-1000-8000-00805f9b34fb": b'c\xc0G\x00"\x04',
  195. },
  196. service_uuids=[
  197. "00001800-0000-1000-8000-00805f9b34fb",
  198. "00001801-0000-1000-8000-00805f9b34fb",
  199. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  200. ],
  201. rssi=-62,
  202. )
  203. result = parse_advertisement_data(ble_device, adv_data)
  204. assert result == SwitchBotAdvertisement(
  205. address="aa:bb:cc:dd:ee:ff",
  206. data={
  207. "rawAdvData": b'c\xc0G\x00"\x04',
  208. "data": {
  209. "calibration": True,
  210. "battery": 71,
  211. "inMotion": False,
  212. "position": 100,
  213. "lightLevel": 2,
  214. "deviceChain": 2,
  215. },
  216. "isEncrypted": False,
  217. "model": "c",
  218. "modelFriendlyName": "Curtain",
  219. "modelName": SwitchbotModel.CURTAIN,
  220. },
  221. device=ble_device,
  222. rssi=-62,
  223. active=True,
  224. )
  225. def test_parse_advertisement_data_curtain_firmware_six_position_100_other_rssi():
  226. """Test parse_advertisement_data with firmware six for curtain position 100 other rssi."""
  227. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  228. adv_data = generate_advertisement_data(
  229. local_name="WoCurtain",
  230. manufacturer_data={
  231. 89: b"\xf5\x98\x94\x08\xa0\xe7",
  232. 2409: b'\xf5\x98\x94\x08\xa0\xe7\xa5\x0fc"\x04',
  233. },
  234. service_data={
  235. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0H\x00\x12\x04",
  236. "0000fd3d-0000-1000-8000-00805f9b34fb": b'c\xc0Gc"\x04',
  237. },
  238. service_uuids=[
  239. "00001800-0000-1000-8000-00805f9b34fb",
  240. "00001801-0000-1000-8000-00805f9b34fb",
  241. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  242. ],
  243. rssi=-67,
  244. )
  245. result = parse_advertisement_data(ble_device, adv_data)
  246. assert result == SwitchBotAdvertisement(
  247. address="aa:bb:cc:dd:ee:ff",
  248. data={
  249. "rawAdvData": b'c\xc0Gc"\x04',
  250. "data": {
  251. "calibration": True,
  252. "battery": 71,
  253. "inMotion": False,
  254. "position": 1,
  255. "lightLevel": 2,
  256. "deviceChain": 2,
  257. },
  258. "isEncrypted": False,
  259. "model": "c",
  260. "modelFriendlyName": "Curtain",
  261. "modelName": SwitchbotModel.CURTAIN,
  262. },
  263. device=ble_device,
  264. rssi=-67,
  265. active=True,
  266. )
  267. def test_parse_advertisement_data_curtain_fully_closed():
  268. """Test parse_advertisement_data with firmware six fully closed."""
  269. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  270. adv_data = generate_advertisement_data(
  271. local_name="WoCurtain",
  272. manufacturer_data={2409: b"\xc1\xc7'}U\xab\"\x0fd\x11\x04"},
  273. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0Sd\x11\x04"},
  274. service_uuids=[
  275. "00001800-0000-1000-8000-00805f9b34fb",
  276. "00001801-0000-1000-8000-00805f9b34fb",
  277. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  278. ],
  279. rssi=1,
  280. )
  281. result = parse_advertisement_data(ble_device, adv_data)
  282. assert result == SwitchBotAdvertisement(
  283. address="aa:bb:cc:dd:ee:ff",
  284. data={
  285. "rawAdvData": b"c\xc0Sd\x11\x04",
  286. "data": {
  287. "calibration": True,
  288. "battery": 83,
  289. "inMotion": False,
  290. "position": 0,
  291. "lightLevel": 1,
  292. "deviceChain": 1,
  293. },
  294. "isEncrypted": False,
  295. "model": "c",
  296. "modelFriendlyName": "Curtain",
  297. "modelName": SwitchbotModel.CURTAIN,
  298. },
  299. device=ble_device,
  300. rssi=1,
  301. active=True,
  302. )
  303. def test_parse_advertisement_data_curtain_fully_open():
  304. """Test parse_advertisement_data with firmware six fully open."""
  305. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  306. adv_data = generate_advertisement_data(
  307. local_name="WoCurtain",
  308. manufacturer_data={2409: b"\xc1\xc7'}U\xab%\x0f\x00\x11\x04"},
  309. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0S\x00\x11\x04"},
  310. service_uuids=[
  311. "00001800-0000-1000-8000-00805f9b34fb",
  312. "00001801-0000-1000-8000-00805f9b34fb",
  313. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  314. ],
  315. rssi=1,
  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"c\xc0S\x00\x11\x04",
  322. "data": {
  323. "calibration": True,
  324. "battery": 83,
  325. "inMotion": False,
  326. "position": 100,
  327. "lightLevel": 1,
  328. "deviceChain": 1,
  329. },
  330. "isEncrypted": False,
  331. "model": "c",
  332. "modelFriendlyName": "Curtain",
  333. "modelName": SwitchbotModel.CURTAIN,
  334. },
  335. device=ble_device,
  336. rssi=1,
  337. active=True,
  338. )
  339. def test_parse_advertisement_data_curtain3():
  340. """Test parse_advertisement_data for curtain 3."""
  341. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  342. adv_data = generate_advertisement_data(
  343. manufacturer_data={
  344. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\xf7\x07\x00\x11\x04\x00\x49"
  345. },
  346. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"{\xc0\x49\x00\x11\x04"},
  347. rssi=-80,
  348. )
  349. result = parse_advertisement_data(ble_device, adv_data)
  350. assert result == SwitchBotAdvertisement(
  351. address="aa:bb:cc:dd:ee:ff",
  352. data={
  353. "rawAdvData": b"{\xc0\x49\x00\x11\x04",
  354. "data": {
  355. "calibration": True,
  356. "battery": 73,
  357. "inMotion": False,
  358. "position": 100,
  359. "lightLevel": 1,
  360. "deviceChain": 1,
  361. },
  362. "isEncrypted": False,
  363. "model": "{",
  364. "modelFriendlyName": "Curtain 3",
  365. "modelName": SwitchbotModel.CURTAIN,
  366. },
  367. device=ble_device,
  368. rssi=-80,
  369. active=True,
  370. )
  371. def test_parse_advertisement_data_curtain3_passive():
  372. """Test parse_advertisement_data for curtain passive."""
  373. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  374. adv_data = generate_advertisement_data(
  375. manufacturer_data={
  376. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\xf7\x07\x00\x11\x04\x00\x49"
  377. },
  378. service_data={},
  379. rssi=-80,
  380. )
  381. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.CURTAIN)
  382. assert result == SwitchBotAdvertisement(
  383. address="aa:bb:cc:dd:ee:ff",
  384. data={
  385. "rawAdvData": None,
  386. "data": {
  387. "calibration": None,
  388. "battery": 73,
  389. "inMotion": False,
  390. "position": 100,
  391. "lightLevel": 1,
  392. "deviceChain": 1,
  393. },
  394. "isEncrypted": False,
  395. "model": "{",
  396. "modelFriendlyName": "Curtain 3",
  397. "modelName": SwitchbotModel.CURTAIN,
  398. },
  399. device=ble_device,
  400. rssi=-80,
  401. active=False,
  402. )
  403. def test_parse_advertisement_data_contact():
  404. """Test parse_advertisement_data for the contact sensor."""
  405. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  406. adv_data = generate_advertisement_data(
  407. manufacturer_data={2409: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  408. service_data={
  409. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@d\x05\x00u\x00\xf8\x12"
  410. },
  411. rssi=-80,
  412. )
  413. result = parse_advertisement_data(ble_device, adv_data)
  414. assert result == SwitchBotAdvertisement(
  415. address="aa:bb:cc:dd:ee:ff",
  416. data={
  417. "rawAdvData": b"d@d\x05\x00u\x00\xf8\x12",
  418. "data": {
  419. "button_count": 2,
  420. "contact_open": True,
  421. "contact_timeout": True,
  422. "is_light": True,
  423. "battery": 100,
  424. "motion_detected": True,
  425. "tested": False,
  426. },
  427. "isEncrypted": False,
  428. "model": "d",
  429. "modelFriendlyName": "Contact Sensor",
  430. "modelName": SwitchbotModel.CONTACT_SENSOR,
  431. },
  432. device=ble_device,
  433. rssi=-80,
  434. active=True,
  435. )
  436. def test_parse_advertisement_data_empty():
  437. """Test parse_advertisement_data with empty data does not blow up."""
  438. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  439. adv_data = generate_advertisement_data(
  440. manufacturer_data={2403: b"\xe7\xabF\xac\x8f\x92|\x0f\x00\x11\x04"},
  441. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b""},
  442. )
  443. result = parse_advertisement_data(ble_device, adv_data)
  444. assert result is None
  445. def test_new_bot_firmware():
  446. """Test parsing adv data from new bot firmware."""
  447. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  448. adv_data = generate_advertisement_data(
  449. manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"},
  450. service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10\xe1"},
  451. service_uuids=["CBA20D00-224D-11E6-9FB8-0002A5D5C51B"],
  452. rssi=-90,
  453. )
  454. result = parse_advertisement_data(ble_device, adv_data)
  455. assert result == SwitchBotAdvertisement(
  456. address="aa:bb:cc:dd:ee:ff",
  457. data={
  458. "rawAdvData": b"H\x10\xe1",
  459. "data": {"switchMode": False, "isOn": False, "battery": 97},
  460. "model": "H",
  461. "isEncrypted": False,
  462. "modelFriendlyName": "Bot",
  463. "modelName": SwitchbotModel.BOT,
  464. },
  465. device=ble_device,
  466. rssi=-90,
  467. active=True,
  468. )
  469. def test_parse_advertisement_data_curtain_firmware_six_fully_closed():
  470. """Test parse_advertisement_data with firmware six fully closed."""
  471. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  472. adv_data = generate_advertisement_data(
  473. local_name="WoCurtain",
  474. manufacturer_data={
  475. 89: b"\xcc\xf4\xc4\xf9\xacl",
  476. 2409: b"\xcc\xf4\xc4\xf9\xacl\xeb\x0fd\x12\x04",
  477. },
  478. service_data={
  479. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Yd\x11\x04",
  480. "0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0dd\x12\x04",
  481. },
  482. service_uuids=[
  483. "00001800-0000-1000-8000-00805f9b34fb",
  484. "00001801-0000-1000-8000-00805f9b34fb",
  485. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  486. ],
  487. rssi=-2,
  488. )
  489. result = parse_advertisement_data(ble_device, adv_data)
  490. assert result == SwitchBotAdvertisement(
  491. address="aa:bb:cc:dd:ee:ff",
  492. data={
  493. "rawAdvData": b"c\xc0dd\x12\x04",
  494. "data": {
  495. "calibration": True,
  496. "battery": 100,
  497. "inMotion": False,
  498. "position": 0,
  499. "lightLevel": 1,
  500. "deviceChain": 2,
  501. },
  502. "isEncrypted": False,
  503. "model": "c",
  504. "modelFriendlyName": "Curtain",
  505. "modelName": SwitchbotModel.CURTAIN,
  506. },
  507. device=ble_device,
  508. rssi=-2,
  509. active=True,
  510. )
  511. def test_parse_advertisement_data_curtain_firmware_six_fully_open():
  512. """Test parse_advertisement_data with firmware six fully open."""
  513. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  514. adv_data = generate_advertisement_data(
  515. local_name="WoCurtain",
  516. manufacturer_data={
  517. 89: b"\xcc\xf4\xc4\xf9\xacl",
  518. 2409: b"\xcc\xf4\xc4\xf9\xacl\xe2\x0f\x00\x12\x04",
  519. },
  520. service_data={
  521. "00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Yd\x11\x04",
  522. "0000fd3d-0000-1000-8000-00805f9b34fb": b"c\xc0d\x00\x12\x04",
  523. },
  524. service_uuids=[
  525. "00001800-0000-1000-8000-00805f9b34fb",
  526. "00001801-0000-1000-8000-00805f9b34fb",
  527. "cba20d00-224d-11e6-9fb8-0002a5d5c51b",
  528. ],
  529. rssi=-2,
  530. )
  531. result = parse_advertisement_data(ble_device, adv_data)
  532. assert result == SwitchBotAdvertisement(
  533. address="aa:bb:cc:dd:ee:ff",
  534. data={
  535. "rawAdvData": b"c\xc0d\x00\x12\x04",
  536. "data": {
  537. "calibration": True,
  538. "battery": 100,
  539. "inMotion": False,
  540. "position": 100,
  541. "lightLevel": 1,
  542. "deviceChain": 2,
  543. },
  544. "isEncrypted": False,
  545. "model": "c",
  546. "modelFriendlyName": "Curtain",
  547. "modelName": SwitchbotModel.CURTAIN,
  548. },
  549. device=ble_device,
  550. rssi=-2,
  551. active=True,
  552. )
  553. def test_contact_sensor_mfr():
  554. """Test parsing adv data from new bot firmware."""
  555. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  556. adv_data = generate_advertisement_data(
  557. manufacturer_data={2409: b"\xcb9\xcd\xc4=FA,\x00F\x01\x8f\xc4"},
  558. service_data={
  559. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d\x00\xda\x04\x00F\x01\x8f\xc4"
  560. },
  561. tx_power=-127,
  562. rssi=-70,
  563. )
  564. result = parse_advertisement_data(ble_device, adv_data)
  565. assert result == SwitchBotAdvertisement(
  566. address="aa:bb:cc:dd:ee:ff",
  567. data={
  568. "data": {
  569. "battery": 90,
  570. "button_count": 4,
  571. "contact_open": True,
  572. "contact_timeout": True,
  573. "is_light": False,
  574. "motion_detected": False,
  575. "tested": False,
  576. },
  577. "isEncrypted": False,
  578. "model": "d",
  579. "modelFriendlyName": "Contact Sensor",
  580. "modelName": SwitchbotModel.CONTACT_SENSOR,
  581. "rawAdvData": b"d\x00\xda\x04\x00F\x01\x8f\xc4",
  582. },
  583. device=ble_device,
  584. rssi=-70,
  585. active=True,
  586. )
  587. def test_contact_sensor_mfr_no_service_data():
  588. """Test contact sensor with passive data only."""
  589. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  590. adv_data = generate_advertisement_data(
  591. manufacturer_data={2409: b"\xcb9\xcd\xc4=FA,\x00F\x01\x8f\xc4"},
  592. service_data={},
  593. tx_power=-127,
  594. rssi=-70,
  595. )
  596. result = parse_advertisement_data(ble_device, adv_data)
  597. # Passive detection of contact sensor is not supported
  598. # anymore since the Switchbot Curtain v3 was released
  599. # which uses the heuristics for the contact sensor.
  600. assert result is None
  601. def test_contact_sensor_srv():
  602. """Test parsing adv data from new bot firmware."""
  603. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  604. adv_data = generate_advertisement_data(
  605. service_data={
  606. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d\x00\xda\x04\x00F\x01\x8f\xc4"
  607. },
  608. tx_power=-127,
  609. rssi=-70,
  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. "battery": 90,
  617. "button_count": 4,
  618. "contact_open": True,
  619. "contact_timeout": True,
  620. "is_light": False,
  621. "motion_detected": False,
  622. "tested": False,
  623. },
  624. "isEncrypted": False,
  625. "model": "d",
  626. "modelFriendlyName": "Contact Sensor",
  627. "modelName": SwitchbotModel.CONTACT_SENSOR,
  628. "rawAdvData": b"d\x00\xda\x04\x00F\x01\x8f\xc4",
  629. },
  630. device=ble_device,
  631. rssi=-70,
  632. active=True,
  633. )
  634. def test_contact_sensor_open():
  635. """Test parsing mfr adv data from new bot firmware."""
  636. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  637. adv_data = generate_advertisement_data(
  638. manufacturer_data={2409: b"\xcb9\xcd\xc4=F\x84\x9c\x00\x17\x00QD"},
  639. service_data={
  640. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@\xda\x02\x00\x17\x00QD"
  641. },
  642. tx_power=-127,
  643. rssi=-59,
  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. "battery": 90,
  651. "button_count": 4,
  652. "contact_open": True,
  653. "contact_timeout": False,
  654. "is_light": False,
  655. "motion_detected": True,
  656. "tested": False,
  657. },
  658. "isEncrypted": False,
  659. "model": "d",
  660. "modelFriendlyName": "Contact Sensor",
  661. "modelName": SwitchbotModel.CONTACT_SENSOR,
  662. "rawAdvData": b"d@\xda\x02\x00\x17\x00QD",
  663. },
  664. device=ble_device,
  665. rssi=-59,
  666. active=True,
  667. )
  668. def test_contact_sensor_closed():
  669. """Test parsing mfr adv data from new bot firmware."""
  670. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  671. adv_data = generate_advertisement_data(
  672. manufacturer_data={2409: b"\xcb9\xcd\xc4=F\x89\x8c\x00+\x00\x19\x84"},
  673. service_data={
  674. "0000fd3d-0000-1000-8000-00805f9b34fb": b"d@\xda\x00\x00+\x00\x19\x84"
  675. },
  676. tx_power=-127,
  677. rssi=-50,
  678. )
  679. result = parse_advertisement_data(ble_device, adv_data)
  680. assert result == SwitchBotAdvertisement(
  681. address="aa:bb:cc:dd:ee:ff",
  682. data={
  683. "data": {
  684. "battery": 90,
  685. "button_count": 4,
  686. "contact_open": False,
  687. "contact_timeout": False,
  688. "is_light": False,
  689. "motion_detected": True,
  690. "tested": False,
  691. },
  692. "isEncrypted": False,
  693. "model": "d",
  694. "modelFriendlyName": "Contact Sensor",
  695. "modelName": SwitchbotModel.CONTACT_SENSOR,
  696. "rawAdvData": b"d@\xda\x00\x00+\x00\x19\x84",
  697. },
  698. device=ble_device,
  699. rssi=-50,
  700. active=True,
  701. )
  702. def test_switchbot_passive():
  703. """Test parsing switchbot as passive."""
  704. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  705. adv_data = generate_advertisement_data(
  706. manufacturer_data={89: bytes.fromhex("d51cfb397856")},
  707. service_data={},
  708. tx_power=-127,
  709. rssi=-50,
  710. )
  711. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.BOT)
  712. assert result == SwitchBotAdvertisement(
  713. address="aa:bb:cc:dd:ee:ff",
  714. data={
  715. "data": {
  716. "battery": None,
  717. "switchMode": None,
  718. "isOn": None,
  719. },
  720. "isEncrypted": False,
  721. "model": "H",
  722. "modelFriendlyName": "Bot",
  723. "modelName": SwitchbotModel.BOT,
  724. "rawAdvData": None,
  725. },
  726. device=ble_device,
  727. rssi=-50,
  728. active=False,
  729. )
  730. def test_bulb_active():
  731. """Test parsing bulb as active."""
  732. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  733. adv_data = generate_advertisement_data(
  734. manufacturer_data={2409: b"\x84\xf7\x03\xb4\xcbz\x03\xe4!\x00\x00"},
  735. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"u\x00d"},
  736. tx_power=-127,
  737. rssi=-50,
  738. )
  739. result = parse_advertisement_data(ble_device, adv_data)
  740. assert result == SwitchBotAdvertisement(
  741. address="aa:bb:cc:dd:ee:ff",
  742. data={
  743. "data": {
  744. "brightness": 100,
  745. "color_mode": 1,
  746. "delay": False,
  747. "isOn": True,
  748. "loop_index": 0,
  749. "preset": False,
  750. "sequence_number": 3,
  751. "speed": 0,
  752. },
  753. "isEncrypted": False,
  754. "model": "u",
  755. "modelFriendlyName": "Color Bulb",
  756. "modelName": SwitchbotModel.COLOR_BULB,
  757. "rawAdvData": b"u\x00d",
  758. },
  759. device=ble_device,
  760. rssi=-50,
  761. active=True,
  762. )
  763. def test_wosensor_passive_and_active():
  764. """Test parsing wosensor as passive with active data as well."""
  765. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  766. adv_data = generate_advertisement_data(
  767. manufacturer_data={2409: b"\xd7\xc1}]\xebC\xde\x03\x06\x985"},
  768. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\xe4\x06\x985"},
  769. tx_power=-127,
  770. rssi=-50,
  771. )
  772. result = parse_advertisement_data(ble_device, adv_data)
  773. assert result == SwitchBotAdvertisement(
  774. address="aa:bb:cc:dd:ee:ff",
  775. data={
  776. "data": {
  777. "battery": 100,
  778. "fahrenheit": False,
  779. "humidity": 53,
  780. "temp": {"c": 24.6, "f": 76.28},
  781. "temperature": 24.6,
  782. },
  783. "isEncrypted": False,
  784. "model": "T",
  785. "modelFriendlyName": "Meter",
  786. "modelName": SwitchbotModel.METER,
  787. "rawAdvData": b"T\x00\xe4\x06\x985",
  788. },
  789. device=ble_device,
  790. rssi=-50,
  791. active=True,
  792. )
  793. def test_wosensor_active():
  794. """Test parsing wosensor with active data as well."""
  795. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  796. adv_data = generate_advertisement_data(
  797. manufacturer_data={},
  798. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\xe4\x06\x985"},
  799. tx_power=-127,
  800. rssi=-50,
  801. )
  802. result = parse_advertisement_data(ble_device, adv_data)
  803. assert result == SwitchBotAdvertisement(
  804. address="aa:bb:cc:dd:ee:ff",
  805. data={
  806. "data": {
  807. "battery": 100,
  808. "fahrenheit": False,
  809. "humidity": 53,
  810. "temp": {"c": 24.6, "f": 76.28},
  811. "temperature": 24.6,
  812. },
  813. "isEncrypted": False,
  814. "model": "T",
  815. "modelFriendlyName": "Meter",
  816. "modelName": SwitchbotModel.METER,
  817. "rawAdvData": b"T\x00\xe4\x06\x985",
  818. },
  819. device=ble_device,
  820. rssi=-50,
  821. active=True,
  822. )
  823. def test_wosensor_passive_only():
  824. """Test parsing wosensor with only passive data."""
  825. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  826. adv_data = generate_advertisement_data(
  827. manufacturer_data={2409: b"\xd7\xc1}]\xebC\xde\x03\x06\x985"},
  828. service_data={},
  829. tx_power=-127,
  830. rssi=-50,
  831. )
  832. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.METER)
  833. assert result == SwitchBotAdvertisement(
  834. address="aa:bb:cc:dd:ee:ff",
  835. data={
  836. "data": {
  837. "battery": None,
  838. "fahrenheit": False,
  839. "humidity": 53,
  840. "temp": {"c": 24.6, "f": 76.28},
  841. "temperature": 24.6,
  842. },
  843. "isEncrypted": False,
  844. "model": "i",
  845. "modelFriendlyName": "Meter Plus",
  846. "modelName": SwitchbotModel.METER,
  847. "rawAdvData": None,
  848. },
  849. device=ble_device,
  850. rssi=-50,
  851. active=False,
  852. )
  853. def test_wosensor_active_zero_data():
  854. """Test parsing wosensor with active data but all values are zero."""
  855. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  856. adv_data = generate_advertisement_data(
  857. manufacturer_data={},
  858. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00\x00\x00\x00\x00"},
  859. tx_power=-127,
  860. rssi=-50,
  861. )
  862. result = parse_advertisement_data(ble_device, adv_data)
  863. assert result == SwitchBotAdvertisement(
  864. address="aa:bb:cc:dd:ee:ff",
  865. data={
  866. "data": {},
  867. "isEncrypted": False,
  868. "model": "T",
  869. "rawAdvData": b"T\x00\x00\x00\x00\x00",
  870. },
  871. device=ble_device,
  872. rssi=-50,
  873. active=True,
  874. )
  875. def test_wosensor_active_positive_battery_and_zero_data():
  876. """Test parsing wosensor with active data and positive battery but all other values are zero."""
  877. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  878. adv_data = generate_advertisement_data(
  879. manufacturer_data={},
  880. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00P\x00\x00\x00"},
  881. tx_power=-127,
  882. rssi=-50,
  883. )
  884. result = parse_advertisement_data(ble_device, adv_data)
  885. assert result == SwitchBotAdvertisement(
  886. address="aa:bb:cc:dd:ee:ff",
  887. data={
  888. "data": {},
  889. "isEncrypted": False,
  890. "model": "T",
  891. "rawAdvData": b"T\x00P\x00\x00\x00",
  892. },
  893. device=ble_device,
  894. rssi=-50,
  895. active=True,
  896. )
  897. def test_wohub2_passive_and_active():
  898. """Test parsing wosensor as passive with active data as well."""
  899. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  900. adv_data = generate_advertisement_data(
  901. manufacturer_data={
  902. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x00\xfffT\x1a\xf1\x82\x07\x9a2\x00"
  903. },
  904. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"v\x00"},
  905. tx_power=-127,
  906. rssi=-50,
  907. )
  908. result = parse_advertisement_data(ble_device, adv_data)
  909. assert result == SwitchBotAdvertisement(
  910. address="aa:bb:cc:dd:ee:ff",
  911. data={
  912. "data": {
  913. "fahrenheit": False,
  914. "humidity": 50,
  915. "lightLevel": 2,
  916. "illuminance": 10,
  917. "temp": {"c": 26.7, "f": 80.06},
  918. "temperature": 26.7,
  919. },
  920. "isEncrypted": False,
  921. "model": "v",
  922. "modelFriendlyName": "Hub 2",
  923. "modelName": SwitchbotModel.HUB2,
  924. "rawAdvData": b"v\x00",
  925. },
  926. device=ble_device,
  927. rssi=-50,
  928. active=True,
  929. )
  930. def test_woiosensor_passive_and_active():
  931. """Test parsing woiosensor as passive with active data as well."""
  932. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  933. adv_data = generate_advertisement_data(
  934. manufacturer_data={2409: b"\xaa\xbb\xcc\xdd\xee\xff\xe0\x0f\x06\x985\x00"},
  935. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"w\x00\xe4"},
  936. tx_power=-127,
  937. rssi=-50,
  938. )
  939. result = parse_advertisement_data(ble_device, adv_data)
  940. assert result == SwitchBotAdvertisement(
  941. address="aa:bb:cc:dd:ee:ff",
  942. data={
  943. "data": {
  944. "battery": 100,
  945. "fahrenheit": False,
  946. "humidity": 53,
  947. "temp": {"c": 24.6, "f": 76.28},
  948. "temperature": 24.6,
  949. },
  950. "isEncrypted": False,
  951. "model": "w",
  952. "modelFriendlyName": "Indoor/Outdoor Meter",
  953. "modelName": SwitchbotModel.IO_METER,
  954. "rawAdvData": b"w\x00\xe4",
  955. },
  956. device=ble_device,
  957. rssi=-50,
  958. active=True,
  959. )
  960. def test_woiosensor_passive_only():
  961. """Test parsing woiosensor with only passive data."""
  962. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  963. adv_data = generate_advertisement_data(
  964. manufacturer_data={2409: b"\xaa\xbb\xcc\xdd\xee\xff\xe0\x0f\x06\x985\x00"},
  965. service_data={},
  966. tx_power=-127,
  967. rssi=-50,
  968. )
  969. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.IO_METER)
  970. assert result == SwitchBotAdvertisement(
  971. address="aa:bb:cc:dd:ee:ff",
  972. data={
  973. "data": {
  974. "battery": None,
  975. "fahrenheit": False,
  976. "humidity": 53,
  977. "temp": {"c": 24.6, "f": 76.28},
  978. "temperature": 24.6,
  979. },
  980. "isEncrypted": False,
  981. "model": "w",
  982. "modelFriendlyName": "Indoor/Outdoor Meter",
  983. "modelName": SwitchbotModel.IO_METER,
  984. "rawAdvData": None,
  985. },
  986. device=ble_device,
  987. rssi=-50,
  988. active=False,
  989. )
  990. def test_motion_sensor_clear():
  991. """Test parsing motion sensor with clear data."""
  992. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  993. adv_data = generate_advertisement_data(
  994. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
  995. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x00f\x01"},
  996. tx_power=-127,
  997. rssi=-87,
  998. )
  999. result = parse_advertisement_data(
  1000. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1001. )
  1002. assert result == SwitchBotAdvertisement(
  1003. address="aa:bb:cc:dd:ee:ff",
  1004. data={
  1005. "data": {
  1006. "battery": 98,
  1007. "iot": 0,
  1008. "is_light": False,
  1009. "led": 0,
  1010. "light_intensity": 1,
  1011. "motion_detected": False,
  1012. "sense_distance": 0,
  1013. "tested": False,
  1014. },
  1015. "isEncrypted": False,
  1016. "model": "s",
  1017. "modelFriendlyName": "Motion Sensor",
  1018. "modelName": SwitchbotModel.MOTION_SENSOR,
  1019. "rawAdvData": b"s\x00\xe2\x00f\x01",
  1020. },
  1021. device=ble_device,
  1022. rssi=-87,
  1023. active=True,
  1024. )
  1025. def test_motion_sensor_clear_passive():
  1026. """Test parsing motion sensor with clear data."""
  1027. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1028. adv_data = generate_advertisement_data(
  1029. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIj\x1c\x00f"},
  1030. service_data={},
  1031. tx_power=-127,
  1032. rssi=-87,
  1033. )
  1034. result = parse_advertisement_data(
  1035. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1036. )
  1037. assert result == SwitchBotAdvertisement(
  1038. address="aa:bb:cc:dd:ee:ff",
  1039. data={
  1040. "data": {
  1041. "battery": None,
  1042. "iot": None,
  1043. "is_light": False,
  1044. "led": None,
  1045. "light_intensity": None,
  1046. "motion_detected": False,
  1047. "sense_distance": None,
  1048. "tested": None,
  1049. },
  1050. "isEncrypted": False,
  1051. "model": "s",
  1052. "modelFriendlyName": "Motion Sensor",
  1053. "modelName": SwitchbotModel.MOTION_SENSOR,
  1054. "rawAdvData": None,
  1055. },
  1056. device=ble_device,
  1057. rssi=-87,
  1058. active=False,
  1059. )
  1060. def test_motion_sensor_motion():
  1061. """Test parsing motion sensor with motion data."""
  1062. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1063. adv_data = generate_advertisement_data(
  1064. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
  1065. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x008\x01"},
  1066. tx_power=-127,
  1067. rssi=-87,
  1068. )
  1069. result = parse_advertisement_data(
  1070. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1071. )
  1072. assert result == SwitchBotAdvertisement(
  1073. address="aa:bb:cc:dd:ee:ff",
  1074. data={
  1075. "data": {
  1076. "battery": 98,
  1077. "iot": 0,
  1078. "is_light": False,
  1079. "led": 0,
  1080. "light_intensity": 1,
  1081. "motion_detected": True,
  1082. "sense_distance": 0,
  1083. "tested": False,
  1084. },
  1085. "isEncrypted": False,
  1086. "model": "s",
  1087. "modelFriendlyName": "Motion Sensor",
  1088. "modelName": SwitchbotModel.MOTION_SENSOR,
  1089. "rawAdvData": b"s@\xe2\x008\x01",
  1090. },
  1091. device=ble_device,
  1092. rssi=-87,
  1093. active=True,
  1094. )
  1095. def test_motion_sensor_motion_passive():
  1096. """Test parsing motion sensor with motion data."""
  1097. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1098. adv_data = generate_advertisement_data(
  1099. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIi\\\x008"},
  1100. service_data={},
  1101. tx_power=-127,
  1102. rssi=-87,
  1103. )
  1104. result = parse_advertisement_data(
  1105. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1106. )
  1107. assert result == SwitchBotAdvertisement(
  1108. address="aa:bb:cc:dd:ee:ff",
  1109. data={
  1110. "data": {
  1111. "battery": None,
  1112. "iot": None,
  1113. "is_light": False,
  1114. "led": None,
  1115. "light_intensity": None,
  1116. "motion_detected": True,
  1117. "sense_distance": None,
  1118. "tested": None,
  1119. },
  1120. "isEncrypted": False,
  1121. "model": "s",
  1122. "modelFriendlyName": "Motion Sensor",
  1123. "modelName": SwitchbotModel.MOTION_SENSOR,
  1124. "rawAdvData": None,
  1125. },
  1126. device=ble_device,
  1127. rssi=-87,
  1128. active=False,
  1129. )
  1130. def test_motion_sensor_is_light_passive():
  1131. """Test parsing motion sensor with motion data."""
  1132. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1133. adv_data = generate_advertisement_data(
  1134. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIs,\x04g"},
  1135. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
  1136. tx_power=-127,
  1137. rssi=-93,
  1138. )
  1139. result = parse_advertisement_data(
  1140. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1141. )
  1142. assert result == SwitchBotAdvertisement(
  1143. address="aa:bb:cc:dd:ee:ff",
  1144. data={
  1145. "data": {
  1146. "battery": 98,
  1147. "iot": 0,
  1148. "is_light": True,
  1149. "led": 0,
  1150. "light_intensity": 2,
  1151. "motion_detected": False,
  1152. "sense_distance": 0,
  1153. "tested": False,
  1154. },
  1155. "isEncrypted": False,
  1156. "model": "s",
  1157. "modelFriendlyName": "Motion Sensor",
  1158. "modelName": SwitchbotModel.MOTION_SENSOR,
  1159. "rawAdvData": b"s\x00\xe2\x04g\x02",
  1160. },
  1161. device=ble_device,
  1162. rssi=-93,
  1163. active=True,
  1164. )
  1165. def test_motion_sensor_is_light_active():
  1166. """Test parsing motion sensor with motion data."""
  1167. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1168. adv_data = generate_advertisement_data(
  1169. manufacturer_data={},
  1170. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s\x00\xe2\x04g\x02"},
  1171. tx_power=-127,
  1172. rssi=-93,
  1173. )
  1174. result = parse_advertisement_data(
  1175. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1176. )
  1177. assert result == SwitchBotAdvertisement(
  1178. address="aa:bb:cc:dd:ee:ff",
  1179. data={
  1180. "data": {
  1181. "battery": 98,
  1182. "iot": 0,
  1183. "is_light": True,
  1184. "led": 0,
  1185. "light_intensity": 2,
  1186. "motion_detected": False,
  1187. "sense_distance": 0,
  1188. "tested": False,
  1189. },
  1190. "isEncrypted": False,
  1191. "model": "s",
  1192. "modelFriendlyName": "Motion Sensor",
  1193. "modelName": SwitchbotModel.MOTION_SENSOR,
  1194. "rawAdvData": b"s\x00\xe2\x04g\x02",
  1195. },
  1196. device=ble_device,
  1197. rssi=-93,
  1198. active=True,
  1199. )
  1200. def test_motion_with_light_detected():
  1201. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1202. adv_data = generate_advertisement_data(
  1203. manufacturer_data={2409: b"\xc0!\x9a\xe8\xbcIvl\x00,"},
  1204. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"s@\xe2\x00,\x02"},
  1205. tx_power=-127,
  1206. rssi=-84,
  1207. )
  1208. result = parse_advertisement_data(
  1209. ble_device, adv_data, SwitchbotModel.MOTION_SENSOR
  1210. )
  1211. assert result == SwitchBotAdvertisement(
  1212. address="aa:bb:cc:dd:ee:ff",
  1213. data={
  1214. "data": {
  1215. "battery": 98,
  1216. "iot": 0,
  1217. "is_light": True,
  1218. "led": 0,
  1219. "light_intensity": 2,
  1220. "motion_detected": True,
  1221. "sense_distance": 0,
  1222. "tested": False,
  1223. },
  1224. "isEncrypted": False,
  1225. "model": "s",
  1226. "modelFriendlyName": "Motion Sensor",
  1227. "modelName": SwitchbotModel.MOTION_SENSOR,
  1228. "rawAdvData": b"s@\xe2\x00,\x02",
  1229. },
  1230. device=ble_device,
  1231. rssi=-84,
  1232. active=True,
  1233. )
  1234. def test_meter_pro_active() -> None:
  1235. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1236. adv_data = generate_advertisement_data(
  1237. manufacturer_data={2409: b"\xb0\xe9\xfeR\xdd\x84\x06d\x08\x97,\x00\x05"},
  1238. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"4\x00d"},
  1239. rssi=-67,
  1240. )
  1241. result = parse_advertisement_data(ble_device, adv_data)
  1242. assert result == SwitchBotAdvertisement(
  1243. address="aa:bb:cc:dd:ee:ff",
  1244. data={
  1245. "data": {
  1246. "battery": 100,
  1247. "fahrenheit": False,
  1248. "humidity": 44,
  1249. "temp": {"c": 23.8, "f": 74.84},
  1250. "temperature": 23.8,
  1251. },
  1252. "isEncrypted": False,
  1253. "model": "4",
  1254. "modelFriendlyName": "Meter Pro",
  1255. "modelName": SwitchbotModel.METER_PRO,
  1256. "rawAdvData": b"4\x00d",
  1257. },
  1258. device=ble_device,
  1259. rssi=-67,
  1260. active=True,
  1261. )
  1262. def test_meter_pro_passive() -> None:
  1263. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1264. adv_data = generate_advertisement_data(
  1265. manufacturer_data={2409: b"\xb0\xe9\xfeR\xdd\x84\x06d\x08\x97,\x00\x05"},
  1266. rssi=-67,
  1267. )
  1268. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.METER_PRO)
  1269. assert result == SwitchBotAdvertisement(
  1270. address="aa:bb:cc:dd:ee:ff",
  1271. data={
  1272. "data": {
  1273. "battery": None,
  1274. "fahrenheit": False,
  1275. "humidity": 44,
  1276. "temp": {"c": 23.8, "f": 74.84},
  1277. "temperature": 23.8,
  1278. },
  1279. "isEncrypted": False,
  1280. "model": "4",
  1281. "modelFriendlyName": "Meter Pro",
  1282. "modelName": SwitchbotModel.METER_PRO,
  1283. "rawAdvData": None,
  1284. },
  1285. device=ble_device,
  1286. rssi=-67,
  1287. active=False,
  1288. )
  1289. def test_meter_pro_c_active() -> None:
  1290. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1291. adv_data = generate_advertisement_data(
  1292. manufacturer_data={
  1293. 2409: b"\xb0\xe9\xfeT2\x15\xb7\xe4\x07\x9b\xa4\x007\x02\xd5\x00"
  1294. },
  1295. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"5\x00d"},
  1296. rssi=-67,
  1297. )
  1298. result = parse_advertisement_data(ble_device, adv_data)
  1299. assert result == SwitchBotAdvertisement(
  1300. address="aa:bb:cc:dd:ee:ff",
  1301. data={
  1302. "data": {
  1303. "battery": 100,
  1304. "fahrenheit": True,
  1305. "humidity": 36,
  1306. "temp": {"c": 27.7, "f": 81.86},
  1307. "temperature": 27.7,
  1308. "co2": 725,
  1309. },
  1310. "isEncrypted": False,
  1311. "model": "5",
  1312. "modelFriendlyName": "Meter Pro CO2",
  1313. "modelName": SwitchbotModel.METER_PRO_C,
  1314. "rawAdvData": b"5\x00d",
  1315. },
  1316. device=ble_device,
  1317. rssi=-67,
  1318. active=True,
  1319. )
  1320. def test_meter_pro_c_passive() -> None:
  1321. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1322. adv_data = generate_advertisement_data(
  1323. manufacturer_data={
  1324. 2409: b"\xb0\xe9\xfeT2\x15\xb7\xe4\x07\x9b\xa4\x007\x02\xd5\x00"
  1325. },
  1326. rssi=-67,
  1327. )
  1328. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.METER_PRO_C)
  1329. assert result == SwitchBotAdvertisement(
  1330. address="aa:bb:cc:dd:ee:ff",
  1331. data={
  1332. "data": {
  1333. "battery": None,
  1334. "fahrenheit": True,
  1335. "humidity": 36,
  1336. "temp": {"c": 27.7, "f": 81.86},
  1337. "temperature": 27.7,
  1338. "co2": 725,
  1339. },
  1340. "isEncrypted": False,
  1341. "model": "5",
  1342. "modelFriendlyName": "Meter Pro CO2",
  1343. "modelName": SwitchbotModel.METER_PRO_C,
  1344. "rawAdvData": None,
  1345. },
  1346. device=ble_device,
  1347. rssi=-67,
  1348. active=False,
  1349. )
  1350. def test_meter_pro_c_co2_out_of_range_dropped() -> None:
  1351. """CO2 readings above sensor spec range (9999 ppm) are dropped as spurious."""
  1352. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1353. adv_data = generate_advertisement_data(
  1354. manufacturer_data={
  1355. 2409: b"\xb0\xe9\xfeT2\x15\xb7\xe4\x07\x9b\xa4\x007\x9c\x40\x00"
  1356. },
  1357. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"5\x00d"},
  1358. rssi=-67,
  1359. )
  1360. result = parse_advertisement_data(ble_device, adv_data)
  1361. assert "co2" not in result.data["data"]
  1362. assert result.data["data"]["temperature"] == 27.7
  1363. assert result.data["data"]["humidity"] == 36
  1364. def test_meter_pro_c_co2_boundary_accepted() -> None:
  1365. """CO2 at the upper bound (9999 ppm) is accepted."""
  1366. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1367. adv_data = generate_advertisement_data(
  1368. manufacturer_data={
  1369. 2409: b"\xb0\xe9\xfeT2\x15\xb7\xe4\x07\x9b\xa4\x007\x27\x0f\x00"
  1370. },
  1371. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"5\x00d"},
  1372. rssi=-67,
  1373. )
  1374. result = parse_advertisement_data(ble_device, adv_data)
  1375. assert result.data["data"]["co2"] == 9999
  1376. def test_parse_advertisement_data_keypad():
  1377. """Test parse_advertisement_data for the keypad."""
  1378. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1379. adv_data = generate_advertisement_data(
  1380. manufacturer_data={2409: b"\xeb\x13\x02\xe6#\x0f\x8fd\x00\x00\x00\x00"},
  1381. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"y\x00d"},
  1382. rssi=-67,
  1383. )
  1384. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.KEYPAD)
  1385. assert result == SwitchBotAdvertisement(
  1386. address="aa:bb:cc:dd:ee:ff",
  1387. data={
  1388. "data": {"attempt_state": 143, "battery": 100},
  1389. "isEncrypted": False,
  1390. "model": "y",
  1391. "modelFriendlyName": "Keypad",
  1392. "modelName": SwitchbotModel.KEYPAD,
  1393. "rawAdvData": b"y\x00d",
  1394. },
  1395. device=ble_device,
  1396. rssi=-67,
  1397. active=True,
  1398. )
  1399. def test_leak_active():
  1400. """Test parse_advertisement_data for the leak detector."""
  1401. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1402. adv_data = generate_advertisement_data(
  1403. manufacturer_data={2409: b"\xc4407Lz\x18N\x98g^\x94Q<\x05\x00\x00\x00\x00"},
  1404. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00N"},
  1405. rssi=-72,
  1406. )
  1407. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1408. assert result == SwitchBotAdvertisement(
  1409. address="aa:bb:cc:dd:ee:ff",
  1410. data={
  1411. "data": {
  1412. "leak": False,
  1413. "tampered": False,
  1414. "battery": 78,
  1415. "low_battery": False,
  1416. },
  1417. "isEncrypted": False,
  1418. "model": "&",
  1419. "modelFriendlyName": "Leak Detector",
  1420. "modelName": SwitchbotModel.LEAK,
  1421. "rawAdvData": b"&\x00N",
  1422. },
  1423. device=ble_device,
  1424. rssi=-72,
  1425. active=True,
  1426. )
  1427. def test_leak_passive():
  1428. """Test parse_advertisement_data for the leak detector."""
  1429. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1430. adv_data = generate_advertisement_data(
  1431. manufacturer_data={2409: b"\xc4407Lz\x18N\x98g^\x94Q<\x05\x00\x00\x00\x00"},
  1432. rssi=-72,
  1433. )
  1434. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1435. assert result == SwitchBotAdvertisement(
  1436. address="aa:bb:cc:dd:ee:ff",
  1437. data={
  1438. "data": {
  1439. "leak": False,
  1440. "tampered": False,
  1441. "battery": 78,
  1442. "low_battery": False,
  1443. },
  1444. "isEncrypted": False,
  1445. "model": "&",
  1446. "modelFriendlyName": "Leak Detector",
  1447. "modelName": SwitchbotModel.LEAK,
  1448. "rawAdvData": None,
  1449. },
  1450. device=ble_device,
  1451. rssi=-72,
  1452. active=False,
  1453. )
  1454. def test_leak_no_leak_detected():
  1455. """Test parse_advertisement_data for the leak detector."""
  1456. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
  1457. adv_data = generate_advertisement_data(
  1458. manufacturer_data={
  1459. 2409: b"\xc4407LzJd\x98ga\xc4\n<\x05\x00\x00\x00\x00"
  1460. }, # no leak, batt
  1461. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
  1462. rssi=-73,
  1463. )
  1464. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1465. assert result == SwitchBotAdvertisement(
  1466. address="aa:bb:cc:dd:ee:ff",
  1467. data={
  1468. "data": {
  1469. "leak": False,
  1470. "tampered": False,
  1471. "battery": 100,
  1472. "low_battery": False,
  1473. },
  1474. "isEncrypted": False,
  1475. "model": "&",
  1476. "modelFriendlyName": "Leak Detector",
  1477. "modelName": SwitchbotModel.LEAK,
  1478. "rawAdvData": b"&\x00d",
  1479. },
  1480. device=ble_device,
  1481. rssi=-73,
  1482. active=True,
  1483. )
  1484. def test_leak_leak_detected():
  1485. """Test parse_advertisement_data for the leak detector."""
  1486. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
  1487. adv_data = generate_advertisement_data(
  1488. manufacturer_data={
  1489. 2409: b"\xc4407LzGd\xf9ga\xc4\x08<\x05\x00\x00\x00\x00"
  1490. }, # leak, batt
  1491. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
  1492. rssi=-73,
  1493. )
  1494. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1495. assert result == SwitchBotAdvertisement(
  1496. address="aa:bb:cc:dd:ee:ff",
  1497. data={
  1498. "data": {
  1499. "leak": True,
  1500. "tampered": False,
  1501. "battery": 100,
  1502. "low_battery": False,
  1503. },
  1504. "isEncrypted": False,
  1505. "model": "&",
  1506. "modelFriendlyName": "Leak Detector",
  1507. "modelName": SwitchbotModel.LEAK,
  1508. "rawAdvData": b"&\x00d",
  1509. },
  1510. device=ble_device,
  1511. rssi=-73,
  1512. active=True,
  1513. )
  1514. def test_leak_low_battery():
  1515. """Test parse_advertisement_data for the leak detector."""
  1516. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
  1517. adv_data = generate_advertisement_data(
  1518. manufacturer_data={
  1519. 2409: b"\xc4407Lz\x02\t\x98\x00\x00\x00\x00<\x05\x00\x00\x00\x00"
  1520. }, # no leak, low battery
  1521. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\x00d"},
  1522. rssi=-73,
  1523. )
  1524. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1525. assert result == SwitchBotAdvertisement(
  1526. address="aa:bb:cc:dd:ee:ff",
  1527. data={
  1528. "data": {
  1529. "leak": False,
  1530. "tampered": False,
  1531. "battery": 9,
  1532. "low_battery": False,
  1533. },
  1534. "isEncrypted": False,
  1535. "model": "&",
  1536. "modelFriendlyName": "Leak Detector",
  1537. "modelName": SwitchbotModel.LEAK,
  1538. "rawAdvData": b"&\x00d",
  1539. },
  1540. device=ble_device,
  1541. rssi=-73,
  1542. active=True,
  1543. )
  1544. def test_leak_real_data_from_ha():
  1545. """Test parse_advertisement_data for the leak detector."""
  1546. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "Any")
  1547. adv_data = generate_advertisement_data(
  1548. manufacturer_data={
  1549. 2409: b"\\xd6407D1\\x02V\\x90\\x00\\x00\\x00\\x00\\x1e\\x05\\x00\\x00\\x00\\x00"
  1550. }, # no leak, low battery
  1551. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"&\\x00V"},
  1552. rssi=-73,
  1553. )
  1554. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.LEAK)
  1555. assert result == SwitchBotAdvertisement(
  1556. address="aa:bb:cc:dd:ee:ff",
  1557. data={
  1558. "data": {
  1559. "leak": True,
  1560. "tampered": False,
  1561. "battery": 68,
  1562. "low_battery": False,
  1563. },
  1564. "isEncrypted": False,
  1565. "model": "&",
  1566. "modelFriendlyName": "Leak Detector",
  1567. "modelName": SwitchbotModel.LEAK,
  1568. "rawAdvData": b"&\\x00V",
  1569. },
  1570. device=ble_device,
  1571. rssi=-73,
  1572. active=True,
  1573. )
  1574. def test_remote_active() -> None:
  1575. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1576. adv_data = generate_advertisement_data(
  1577. manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"},
  1578. service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"b V\x00"},
  1579. service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
  1580. rssi=-95,
  1581. )
  1582. result = parse_advertisement_data(ble_device, adv_data)
  1583. assert result == SwitchBotAdvertisement(
  1584. address="aa:bb:cc:dd:ee:ff",
  1585. data={
  1586. "data": {
  1587. "battery": 86,
  1588. },
  1589. "isEncrypted": False,
  1590. "model": "b",
  1591. "modelFriendlyName": "Remote",
  1592. "modelName": SwitchbotModel.REMOTE,
  1593. "rawAdvData": b"b V\x00",
  1594. },
  1595. device=ble_device,
  1596. rssi=-95,
  1597. active=True,
  1598. )
  1599. def test_remote_passive() -> None:
  1600. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1601. adv_data = generate_advertisement_data(
  1602. manufacturer_data={89: b"\xaa\xbb\xcc\xdd\xee\xff"},
  1603. rssi=-97,
  1604. )
  1605. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.REMOTE)
  1606. assert result == SwitchBotAdvertisement(
  1607. address="aa:bb:cc:dd:ee:ff",
  1608. data={
  1609. "data": {
  1610. "battery": None,
  1611. },
  1612. "isEncrypted": False,
  1613. "model": "b",
  1614. "modelFriendlyName": "Remote",
  1615. "modelName": SwitchbotModel.REMOTE,
  1616. "rawAdvData": None,
  1617. },
  1618. device=ble_device,
  1619. rssi=-97,
  1620. active=False,
  1621. )
  1622. def test_universal_remote_active() -> None:
  1623. """Test Universal Remote active scan parsing."""
  1624. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1625. adv_data = generate_advertisement_data(
  1626. manufacturer_data={
  1627. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x00\x50\x00\x00\x00\x00\x00\x00\x00\x00"
  1628. },
  1629. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"'\x00"},
  1630. service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
  1631. rssi=-66,
  1632. )
  1633. result = parse_advertisement_data(ble_device, adv_data)
  1634. assert result == SwitchBotAdvertisement(
  1635. address="aa:bb:cc:dd:ee:ff",
  1636. data={
  1637. "data": {
  1638. "battery": 80,
  1639. "charging": False,
  1640. },
  1641. "isEncrypted": False,
  1642. "model": "'",
  1643. "modelFriendlyName": "Universal Remote",
  1644. "modelName": SwitchbotModel.UNIVERSAL_REMOTE,
  1645. "rawAdvData": b"'\x00",
  1646. },
  1647. device=ble_device,
  1648. rssi=-66,
  1649. active=True,
  1650. )
  1651. def test_universal_remote_charging() -> None:
  1652. """Test Universal Remote reports the charging bit from byte 14."""
  1653. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1654. adv_data = generate_advertisement_data(
  1655. manufacturer_data={
  1656. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x00\xb7\x00\x00\x00\x00\x00\x00\x00\x00"
  1657. },
  1658. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"'\x00"},
  1659. service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
  1660. rssi=-66,
  1661. )
  1662. result = parse_advertisement_data(ble_device, adv_data)
  1663. assert result is not None
  1664. assert result.data["data"] == {"battery": 55, "charging": True}
  1665. def test_parse_advertisement_data_hubmini_matter():
  1666. """Test parse_advertisement_data for the HubMini Matter."""
  1667. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1668. adv_data = generate_advertisement_data(
  1669. manufacturer_data={
  1670. 2409: b"\xe6\xa1\xcd\x1f[e\x00\x00\x00\x00\x00\x00\x14\x01\x985\x00"
  1671. },
  1672. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"%\x00"},
  1673. rssi=-67,
  1674. )
  1675. result = parse_advertisement_data(
  1676. ble_device, adv_data, SwitchbotModel.HUBMINI_MATTER
  1677. )
  1678. assert result == SwitchBotAdvertisement(
  1679. address="aa:bb:cc:dd:ee:ff",
  1680. data={
  1681. "data": {
  1682. "fahrenheit": False,
  1683. "humidity": 53,
  1684. "temp": {"c": 24.1, "f": 75.38},
  1685. "temperature": 24.1,
  1686. },
  1687. "isEncrypted": False,
  1688. "model": "%",
  1689. "modelFriendlyName": "HubMini Matter",
  1690. "modelName": SwitchbotModel.HUBMINI_MATTER,
  1691. "rawAdvData": b"%\x00",
  1692. },
  1693. device=ble_device,
  1694. rssi=-67,
  1695. active=True,
  1696. )
  1697. def test_parse_advertisement_data_roller_shade():
  1698. """Test parse_advertisement_data for roller shade."""
  1699. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1700. adv_data = generate_advertisement_data(
  1701. manufacturer_data={2409: b"\xb0\xe9\xfeT\x90\x1b,\x08\x9f\x11\x04'\x00"},
  1702. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b",\x00'\x9f\x11\x04"},
  1703. rssi=-80,
  1704. )
  1705. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.ROLLER_SHADE)
  1706. assert result == SwitchBotAdvertisement(
  1707. address="aa:bb:cc:dd:ee:ff",
  1708. data={
  1709. "rawAdvData": b",\x00'\x9f\x11\x04",
  1710. "data": {
  1711. "battery": 39,
  1712. "calibration": True,
  1713. "deviceChain": 1,
  1714. "inMotion": False,
  1715. "lightLevel": 1,
  1716. "position": 69,
  1717. "sequence_number": 44,
  1718. },
  1719. "isEncrypted": False,
  1720. "model": ",",
  1721. "modelFriendlyName": "Roller Shade",
  1722. "modelName": SwitchbotModel.ROLLER_SHADE,
  1723. },
  1724. device=ble_device,
  1725. rssi=-80,
  1726. active=True,
  1727. )
  1728. def test_hubmini_matter_passive() -> None:
  1729. """Test parsing hubmini matter with passive data."""
  1730. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1731. adv_data = generate_advertisement_data(
  1732. manufacturer_data={
  1733. 2409: b"\xe6\xa1\xcd\x1f[e\x00\x00\x00\x00\x00\x00\x14\x01\x985\x00"
  1734. },
  1735. rssi=-97,
  1736. )
  1737. result = parse_advertisement_data(
  1738. ble_device, adv_data, SwitchbotModel.HUBMINI_MATTER
  1739. )
  1740. assert result == SwitchBotAdvertisement(
  1741. address="aa:bb:cc:dd:ee:ff",
  1742. data={
  1743. "data": {
  1744. "fahrenheit": False,
  1745. "humidity": 53,
  1746. "temp": {"c": 24.1, "f": 75.38},
  1747. "temperature": 24.1,
  1748. },
  1749. "isEncrypted": False,
  1750. "model": "%",
  1751. "modelFriendlyName": "HubMini Matter",
  1752. "modelName": SwitchbotModel.HUBMINI_MATTER,
  1753. "rawAdvData": None,
  1754. },
  1755. device=ble_device,
  1756. rssi=-97,
  1757. active=False,
  1758. )
  1759. def test_roller_shade_passive() -> None:
  1760. """Test parsing roller_shade with passive data."""
  1761. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1762. adv_data = generate_advertisement_data(
  1763. manufacturer_data={2409: b"\xb0\xe9\xfeT\x90\x1b,\x08\x9f\x11\x04'\x00"},
  1764. rssi=-97,
  1765. )
  1766. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.ROLLER_SHADE)
  1767. assert result == SwitchBotAdvertisement(
  1768. address="aa:bb:cc:dd:ee:ff",
  1769. data={
  1770. "rawAdvData": None,
  1771. "data": {
  1772. "battery": None,
  1773. "calibration": True,
  1774. "deviceChain": 1,
  1775. "inMotion": False,
  1776. "lightLevel": 1,
  1777. "position": 69,
  1778. "sequence_number": 44,
  1779. },
  1780. "isEncrypted": False,
  1781. "model": ",",
  1782. "modelFriendlyName": "Roller Shade",
  1783. "modelName": SwitchbotModel.ROLLER_SHADE,
  1784. },
  1785. device=ble_device,
  1786. rssi=-97,
  1787. active=False,
  1788. )
  1789. def test_circulator_fan_active() -> None:
  1790. """Test parsing circulator fan with active data."""
  1791. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1792. adv_data = generate_advertisement_data(
  1793. manufacturer_data={2409: b"\xb0\xe9\xfeXY\xa8~LR9"},
  1794. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
  1795. rssi=-97,
  1796. )
  1797. result = parse_advertisement_data(
  1798. ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
  1799. )
  1800. assert result == SwitchBotAdvertisement(
  1801. address="aa:bb:cc:dd:ee:ff",
  1802. data={
  1803. "rawAdvData": b"~\x00R",
  1804. "data": {
  1805. "sequence_number": 126,
  1806. "isOn": False,
  1807. "mode": "baby",
  1808. "nightLight": 3,
  1809. "oscillating": False,
  1810. "oscillating_horizontal": False,
  1811. "oscillating_vertical": False,
  1812. "battery": 82,
  1813. "speed": 57,
  1814. },
  1815. "isEncrypted": False,
  1816. "model": "~",
  1817. "modelFriendlyName": "Circulator Fan",
  1818. "modelName": SwitchbotModel.CIRCULATOR_FAN,
  1819. },
  1820. device=ble_device,
  1821. rssi=-97,
  1822. active=True,
  1823. )
  1824. def test_circulator_fan_passive() -> None:
  1825. """Test parsing circulator fan with passive data."""
  1826. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1827. adv_data = generate_advertisement_data(
  1828. manufacturer_data={2409: b"\xb0\xe9\xfeXY\xa8~LR9"},
  1829. rssi=-97,
  1830. )
  1831. result = parse_advertisement_data(
  1832. ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
  1833. )
  1834. assert result == SwitchBotAdvertisement(
  1835. address="aa:bb:cc:dd:ee:ff",
  1836. data={
  1837. "rawAdvData": None,
  1838. "data": {
  1839. "sequence_number": 126,
  1840. "isOn": False,
  1841. "mode": "baby",
  1842. "nightLight": 3,
  1843. "oscillating": False,
  1844. "oscillating_horizontal": False,
  1845. "oscillating_vertical": False,
  1846. "battery": 82,
  1847. "speed": 57,
  1848. },
  1849. "isEncrypted": False,
  1850. "model": "~",
  1851. "modelFriendlyName": "Circulator Fan",
  1852. "modelName": SwitchbotModel.CIRCULATOR_FAN,
  1853. },
  1854. device=ble_device,
  1855. rssi=-97,
  1856. active=False,
  1857. )
  1858. def test_circulator_fan_pro_active() -> None:
  1859. """
  1860. Test parsing Circulator Fan Pro (W1160) with active data.
  1861. Real W1160 capture (fan on, light off, no swing). The Pro shares the W1071
  1862. Modern Ceiling Fan broadcast layout (battery at offset 7, fan state at 8,
  1863. speed at 9, CCT light at 10, color temp at 11-12) and is routed by its own
  1864. 4-byte service-data suffix (0x00 0x11 0xB3 0x40).
  1865. """
  1866. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1867. adv_data = generate_advertisement_data(
  1868. manufacturer_data={
  1869. 2409: b"\xb0\xe9\xfe\xfd\xc0\xb1\x9a\xd9\x98\x0b\x00\x00\x00\x00\x00\x00"
  1870. },
  1871. service_data={
  1872. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00Y\x00\x11\xb3@"
  1873. },
  1874. rssi=-97,
  1875. )
  1876. result = parse_advertisement_data(
  1877. ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN_PRO
  1878. )
  1879. assert result == SwitchBotAdvertisement(
  1880. address="aa:bb:cc:dd:ee:ff",
  1881. data={
  1882. "rawAdvData": b"\x00\x00Y\x00\x11\xb3@",
  1883. "data": {
  1884. "sequence_number": 154,
  1885. "isOn": True,
  1886. "mode": "normal",
  1887. "night_light_is_on": False,
  1888. "night_light_level": 0,
  1889. "oscillating": False,
  1890. "oscillating_horizontal": False,
  1891. "oscillating_vertical": False,
  1892. "battery": 89,
  1893. "charging": True,
  1894. "speed": 11,
  1895. },
  1896. "isEncrypted": False,
  1897. "model": b"\x00\x11\xb3@",
  1898. "modelFriendlyName": "Circulator Fan Pro",
  1899. "modelName": SwitchbotModel.CIRCULATOR_FAN_PRO,
  1900. },
  1901. device=ble_device,
  1902. rssi=-97,
  1903. active=True,
  1904. )
  1905. def test_circulator_fan_pro_routes_by_service_data_suffix() -> None:
  1906. """The Pro is identified by its service-data suffix without an explicit model."""
  1907. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1908. adv_data = generate_advertisement_data(
  1909. manufacturer_data={
  1910. 2409: b"\xb0\xe9\xfe\xfd\xc0\xb1\x9a\xd9\x98\x0b\x00\x00\x00\x00\x00\x00"
  1911. },
  1912. service_data={
  1913. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00Y\x00\x11\xb3@"
  1914. },
  1915. rssi=-97,
  1916. )
  1917. result = parse_advertisement_data(ble_device, adv_data)
  1918. assert result is not None
  1919. assert result.data["modelName"] == SwitchbotModel.CIRCULATOR_FAN_PRO
  1920. assert result.data["modelFriendlyName"] == "Circulator Fan Pro"
  1921. @pytest.mark.parametrize(
  1922. ("mfr_data", "is_on", "level"),
  1923. [
  1924. # state byte (offset 8): 0x98 off, 0x94 on/high (L1), 0x9c on/low (L2)
  1925. (b"\xb0\xe9\xfe\xfd\xc0\xb1\x9a\xd9\x98\x0b\x00\x00\x00\x00\x00\x00", False, 0),
  1926. (b"\xb0\xe9\xfe\xfd\xc0\xb1\x39\x64\x94\x0b\x00\x00\x00\x00\x00\x00", True, 1),
  1927. (b"\xb0\xe9\xfe\xfd\xc0\xb1\x3b\xe4\x9c\x0b\x00\x00\x00\x00\x00\x00", True, 2),
  1928. ],
  1929. )
  1930. def test_circulator_fan_pro_night_light(
  1931. mfr_data: bytes, is_on: bool, level: int
  1932. ) -> None:
  1933. """The Pro night light is parsed from the fan-state byte (bit2 on, bit3 level)."""
  1934. data = process_circulator_fan_pro(None, mfr_data)
  1935. assert data["night_light_is_on"] is is_on
  1936. assert data["night_light_level"] == level
  1937. @pytest.mark.parametrize("mfr_data", [None, b"\xb0\xe9\xfe\xfd\xc0\xb1\x9a"])
  1938. def test_circulator_fan_pro_short_data(mfr_data: bytes | None) -> None:
  1939. """Short or missing manufacturer data yields an empty parse."""
  1940. assert process_circulator_fan_pro(None, mfr_data) == {}
  1941. def test_circulator_fan_with_empty_data() -> None:
  1942. """Test parsing circulator fan with empty data."""
  1943. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1944. adv_data = generate_advertisement_data(
  1945. manufacturer_data={2409: None},
  1946. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
  1947. rssi=-97,
  1948. )
  1949. result = parse_advertisement_data(
  1950. ble_device, adv_data, SwitchbotModel.CIRCULATOR_FAN
  1951. )
  1952. assert result == SwitchBotAdvertisement(
  1953. address="aa:bb:cc:dd:ee:ff",
  1954. data={
  1955. "rawAdvData": b"~\x00R",
  1956. "data": {},
  1957. "isEncrypted": False,
  1958. "model": "~",
  1959. },
  1960. device=ble_device,
  1961. rssi=-97,
  1962. active=True,
  1963. )
  1964. def test_standing_fan_custom_natural_with_per_axis_oscillation() -> None:
  1965. """
  1966. Standing Fan in CUSTOM_NATURAL (mode 5) + both axes oscillating.
  1967. Uses a realistic 7-byte service_data ending with the 4-byte
  1968. STANDING_FAN suffix so auto-detection (no explicit model arg)
  1969. resolves the model via `_find_model_from_service_data_suffix`.
  1970. """
  1971. # Mode field is bits [6:4] of device_data[1].
  1972. # Mode 5 (custom_natural) = 0b0101 0000, plus isOn=0b1000_0000, plus
  1973. # horizontal=0b10, vertical=0b01 → 0b1101 0011 = 0xD3.
  1974. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1975. adv_data = generate_advertisement_data(
  1976. manufacturer_data={2409: b"\xb0\xe9\xfe\x01\x02\x03~\xd3R9"},
  1977. service_data={
  1978. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x00\x00\x11\x07\x60"
  1979. },
  1980. rssi=-97,
  1981. )
  1982. # No explicit model — exercises suffix-based auto-detection.
  1983. result = parse_advertisement_data(ble_device, adv_data)
  1984. assert result is not None
  1985. assert result.data["modelName"] == SwitchbotModel.STANDING_FAN
  1986. data = result.data["data"]
  1987. assert data["mode"] == "custom_natural"
  1988. assert data["oscillating"] is True
  1989. assert data["oscillating_horizontal"] is True
  1990. assert data["oscillating_vertical"] is True
  1991. def test_k20_active() -> None:
  1992. """Test parsing k20 with active data."""
  1993. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  1994. adv_data = generate_advertisement_data(
  1995. manufacturer_data={2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f"},
  1996. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"},
  1997. rssi=-97,
  1998. )
  1999. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
  2000. assert result == SwitchBotAdvertisement(
  2001. address="aa:bb:cc:dd:ee:ff",
  2002. data={
  2003. "rawAdvData": b".\x00d",
  2004. "data": {
  2005. "sequence_number": 39,
  2006. "soc_version": "1.1.083",
  2007. "step": 0,
  2008. "mqtt_connected": True,
  2009. "battery": 100,
  2010. "work_status": 15,
  2011. },
  2012. "isEncrypted": False,
  2013. "model": ".",
  2014. "modelFriendlyName": "K20 Vacuum",
  2015. "modelName": SwitchbotModel.K20_VACUUM,
  2016. },
  2017. device=ble_device,
  2018. rssi=-97,
  2019. active=True,
  2020. )
  2021. def test_k20_passive() -> None:
  2022. """Test parsing k20 with passive data."""
  2023. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2024. adv_data = generate_advertisement_data(
  2025. manufacturer_data={2409: b"\xb0\xe9\xfe\x01\xf3\x8f'\x01\x11S\x00\x10d\x0f"},
  2026. rssi=-97,
  2027. )
  2028. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
  2029. assert result == SwitchBotAdvertisement(
  2030. address="aa:bb:cc:dd:ee:ff",
  2031. data={
  2032. "rawAdvData": None,
  2033. "data": {
  2034. "sequence_number": 39,
  2035. "soc_version": "1.1.083",
  2036. "step": 0,
  2037. "mqtt_connected": True,
  2038. "battery": 100,
  2039. "work_status": 15,
  2040. },
  2041. "isEncrypted": False,
  2042. "model": ".",
  2043. "modelFriendlyName": "K20 Vacuum",
  2044. "modelName": SwitchbotModel.K20_VACUUM,
  2045. },
  2046. device=ble_device,
  2047. rssi=-97,
  2048. active=False,
  2049. )
  2050. def test_k20_with_empty_data() -> None:
  2051. """Test parsing k20 with empty data."""
  2052. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2053. adv_data = generate_advertisement_data(
  2054. manufacturer_data={2409: None},
  2055. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b".\x00d"},
  2056. rssi=-97,
  2057. )
  2058. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K20_VACUUM)
  2059. assert result == SwitchBotAdvertisement(
  2060. address="aa:bb:cc:dd:ee:ff",
  2061. data={
  2062. "rawAdvData": b".\x00d",
  2063. "data": {},
  2064. "isEncrypted": False,
  2065. "model": ".",
  2066. },
  2067. device=ble_device,
  2068. rssi=-97,
  2069. active=True,
  2070. )
  2071. def test_k10_pro_active() -> None:
  2072. """Test parsing k10 pro with active data."""
  2073. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2074. adv_data = generate_advertisement_data(
  2075. manufacturer_data={2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d"},
  2076. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"},
  2077. rssi=-97,
  2078. )
  2079. result = parse_advertisement_data(
  2080. ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
  2081. )
  2082. assert result == SwitchBotAdvertisement(
  2083. address="aa:bb:cc:dd:ee:ff",
  2084. data={
  2085. "rawAdvData": b"(\x00",
  2086. "data": {
  2087. "sequence_number": 2,
  2088. "dusbin_connected": False,
  2089. "dustbin_bound": False,
  2090. "network_connected": True,
  2091. "battery": 100,
  2092. "work_status": 0,
  2093. },
  2094. "isEncrypted": False,
  2095. "model": "(",
  2096. "modelFriendlyName": "K10+ Pro Vacuum",
  2097. "modelName": SwitchbotModel.K10_PRO_VACUUM,
  2098. },
  2099. device=ble_device,
  2100. rssi=-97,
  2101. active=True,
  2102. )
  2103. def test_k10_pro_passive() -> None:
  2104. """Test parsing k10 pro with passive data."""
  2105. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2106. adv_data = generate_advertisement_data(
  2107. manufacturer_data={2409: b"\xb0\xe9\xfeP\x8d\x8d\x02 d"},
  2108. rssi=-97,
  2109. )
  2110. result = parse_advertisement_data(
  2111. ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
  2112. )
  2113. assert result == SwitchBotAdvertisement(
  2114. address="aa:bb:cc:dd:ee:ff",
  2115. data={
  2116. "rawAdvData": None,
  2117. "data": {
  2118. "sequence_number": 2,
  2119. "dusbin_connected": False,
  2120. "dustbin_bound": False,
  2121. "network_connected": True,
  2122. "battery": 100,
  2123. "work_status": 0,
  2124. },
  2125. "isEncrypted": False,
  2126. "model": "(",
  2127. "modelFriendlyName": "K10+ Pro Vacuum",
  2128. "modelName": SwitchbotModel.K10_PRO_VACUUM,
  2129. },
  2130. device=ble_device,
  2131. rssi=-97,
  2132. active=False,
  2133. )
  2134. def test_k10_pro_with_empty_data() -> None:
  2135. """Test parsing k10 pro with empty data."""
  2136. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2137. adv_data = generate_advertisement_data(
  2138. manufacturer_data={2409: None},
  2139. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"(\x00"},
  2140. rssi=-97,
  2141. )
  2142. result = parse_advertisement_data(
  2143. ble_device, adv_data, SwitchbotModel.K10_PRO_VACUUM
  2144. )
  2145. assert result == SwitchBotAdvertisement(
  2146. address="aa:bb:cc:dd:ee:ff",
  2147. data={
  2148. "rawAdvData": b"(\x00",
  2149. "data": {},
  2150. "isEncrypted": False,
  2151. "model": "(",
  2152. },
  2153. device=ble_device,
  2154. rssi=-97,
  2155. active=True,
  2156. )
  2157. def test_k10_active() -> None:
  2158. """Test parsing k10+ with active data."""
  2159. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2160. adv_data = generate_advertisement_data(
  2161. manufacturer_data={2409: b"\xca8\x06\xa9_\xf1\x02 d"},
  2162. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"},
  2163. rssi=-97,
  2164. )
  2165. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
  2166. assert result == SwitchBotAdvertisement(
  2167. address="aa:bb:cc:dd:ee:ff",
  2168. data={
  2169. "rawAdvData": b"}\x00",
  2170. "data": {
  2171. "sequence_number": 2,
  2172. "dusbin_connected": False,
  2173. "dustbin_bound": False,
  2174. "network_connected": True,
  2175. "battery": 100,
  2176. "work_status": 0,
  2177. },
  2178. "isEncrypted": False,
  2179. "model": "}",
  2180. "modelFriendlyName": "K10+ Vacuum",
  2181. "modelName": SwitchbotModel.K10_VACUUM,
  2182. },
  2183. device=ble_device,
  2184. rssi=-97,
  2185. active=True,
  2186. )
  2187. def test_k10_passive() -> None:
  2188. """Test parsing k10+ with passive data."""
  2189. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2190. adv_data = generate_advertisement_data(
  2191. manufacturer_data={2409: b"\xca8\x06\xa9_\xf1\x02 d"},
  2192. rssi=-97,
  2193. )
  2194. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
  2195. assert result == SwitchBotAdvertisement(
  2196. address="aa:bb:cc:dd:ee:ff",
  2197. data={
  2198. "rawAdvData": None,
  2199. "data": {
  2200. "sequence_number": 2,
  2201. "dusbin_connected": False,
  2202. "dustbin_bound": False,
  2203. "network_connected": True,
  2204. "battery": 100,
  2205. "work_status": 0,
  2206. },
  2207. "isEncrypted": False,
  2208. "model": "}",
  2209. "modelFriendlyName": "K10+ Vacuum",
  2210. "modelName": SwitchbotModel.K10_VACUUM,
  2211. },
  2212. device=ble_device,
  2213. rssi=-97,
  2214. active=False,
  2215. )
  2216. def test_k10_with_empty_data() -> None:
  2217. """Test parsing k10+ with empty data."""
  2218. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2219. adv_data = generate_advertisement_data(
  2220. manufacturer_data={2409: None},
  2221. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"}\x00"},
  2222. rssi=-97,
  2223. )
  2224. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.K10_VACUUM)
  2225. assert result == SwitchBotAdvertisement(
  2226. address="aa:bb:cc:dd:ee:ff",
  2227. data={
  2228. "rawAdvData": b"}\x00",
  2229. "data": {},
  2230. "isEncrypted": False,
  2231. "model": "}",
  2232. },
  2233. device=ble_device,
  2234. rssi=-97,
  2235. active=True,
  2236. )
  2237. def test_k10_pro_combo_active() -> None:
  2238. """Test parsing k10+ pro combo with active data."""
  2239. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2240. adv_data = generate_advertisement_data(
  2241. manufacturer_data={
  2242. 2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01"
  2243. },
  2244. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"},
  2245. rssi=-97,
  2246. )
  2247. result = parse_advertisement_data(
  2248. ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
  2249. )
  2250. assert result == SwitchBotAdvertisement(
  2251. address="aa:bb:cc:dd:ee:ff",
  2252. data={
  2253. "rawAdvData": b"3\x00\x00",
  2254. "data": {
  2255. "sequence_number": 11,
  2256. "soc_version": "1.0.945",
  2257. "step": 1,
  2258. "mqtt_connected": True,
  2259. "battery": 56,
  2260. "work_status": 1,
  2261. },
  2262. "isEncrypted": False,
  2263. "model": "3",
  2264. "modelFriendlyName": "K10+ Pro Combo Vacuum",
  2265. "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
  2266. },
  2267. device=ble_device,
  2268. rssi=-97,
  2269. active=True,
  2270. )
  2271. def test_k10_pro_combo_passive() -> None:
  2272. """Test parsing k10+ pro combo with passive data."""
  2273. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2274. adv_data = generate_advertisement_data(
  2275. manufacturer_data={
  2276. 2409: b"\xb0\xe9\xfe\x01\xf4\x1d\x0b\x01\x01\xb1\x03\x118\x01"
  2277. },
  2278. rssi=-97,
  2279. )
  2280. result = parse_advertisement_data(
  2281. ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
  2282. )
  2283. assert result == SwitchBotAdvertisement(
  2284. address="aa:bb:cc:dd:ee:ff",
  2285. data={
  2286. "rawAdvData": None,
  2287. "data": {
  2288. "sequence_number": 11,
  2289. "soc_version": "1.0.945",
  2290. "step": 1,
  2291. "mqtt_connected": True,
  2292. "battery": 56,
  2293. "work_status": 1,
  2294. },
  2295. "isEncrypted": False,
  2296. "model": "3",
  2297. "modelFriendlyName": "K10+ Pro Combo Vacuum",
  2298. "modelName": SwitchbotModel.K10_PRO_COMBO_VACUUM,
  2299. },
  2300. device=ble_device,
  2301. rssi=-97,
  2302. active=False,
  2303. )
  2304. def test_k10_pro_combo_with_empty_data() -> None:
  2305. """Test parsing k10+ pro combo with empty data."""
  2306. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2307. adv_data = generate_advertisement_data(
  2308. manufacturer_data={2409: None},
  2309. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"3\x00\x00"},
  2310. rssi=-97,
  2311. )
  2312. result = parse_advertisement_data(
  2313. ble_device, adv_data, SwitchbotModel.K10_PRO_COMBO_VACUUM
  2314. )
  2315. assert result == SwitchBotAdvertisement(
  2316. address="aa:bb:cc:dd:ee:ff",
  2317. data={
  2318. "rawAdvData": b"3\x00\x00",
  2319. "data": {},
  2320. "isEncrypted": False,
  2321. "model": "3",
  2322. },
  2323. device=ble_device,
  2324. rssi=-97,
  2325. active=True,
  2326. )
  2327. def test_s10_active() -> None:
  2328. """Test parsing s10 with active data."""
  2329. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2330. adv_data = generate_advertisement_data(
  2331. manufacturer_data={2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02"},
  2332. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"},
  2333. rssi=-97,
  2334. )
  2335. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
  2336. assert result == SwitchBotAdvertisement(
  2337. address="aa:bb:cc:dd:ee:ff",
  2338. data={
  2339. "rawAdvData": b"z\x00\x00",
  2340. "data": {
  2341. "sequence_number": 10,
  2342. "soc_version": "1.1.005",
  2343. "step": 0,
  2344. "mqtt_connected": True,
  2345. "battery": 77,
  2346. "work_status": 2,
  2347. },
  2348. "isEncrypted": False,
  2349. "model": "z",
  2350. "modelFriendlyName": "S10 Vacuum",
  2351. "modelName": SwitchbotModel.S10_VACUUM,
  2352. },
  2353. device=ble_device,
  2354. rssi=-97,
  2355. active=True,
  2356. )
  2357. def test_s10_passive() -> None:
  2358. """Test parsing s10 with passive data."""
  2359. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2360. adv_data = generate_advertisement_data(
  2361. manufacturer_data={2409: b"\xb0\xe9\xfe\x00\x08|\n\x01\x11\x05\x00\x10M\x02"},
  2362. rssi=-97,
  2363. )
  2364. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
  2365. assert result == SwitchBotAdvertisement(
  2366. address="aa:bb:cc:dd:ee:ff",
  2367. data={
  2368. "rawAdvData": None,
  2369. "data": {
  2370. "sequence_number": 10,
  2371. "soc_version": "1.1.005",
  2372. "step": 0,
  2373. "mqtt_connected": True,
  2374. "battery": 77,
  2375. "work_status": 2,
  2376. },
  2377. "isEncrypted": False,
  2378. "model": "z",
  2379. "modelFriendlyName": "S10 Vacuum",
  2380. "modelName": SwitchbotModel.S10_VACUUM,
  2381. },
  2382. device=ble_device,
  2383. rssi=-97,
  2384. active=False,
  2385. )
  2386. def test_s10_with_empty_data() -> None:
  2387. """Test parsing s10 with empty data."""
  2388. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2389. adv_data = generate_advertisement_data(
  2390. manufacturer_data={2409: None},
  2391. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"z\x00\x00"},
  2392. rssi=-97,
  2393. )
  2394. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.S10_VACUUM)
  2395. assert result == SwitchBotAdvertisement(
  2396. address="aa:bb:cc:dd:ee:ff",
  2397. data={
  2398. "rawAdvData": b"z\x00\x00",
  2399. "data": {},
  2400. "isEncrypted": False,
  2401. "model": "z",
  2402. },
  2403. device=ble_device,
  2404. rssi=-97,
  2405. active=True,
  2406. )
  2407. @pytest.mark.parametrize(
  2408. "test_case",
  2409. [
  2410. AdvTestCase(
  2411. b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00",
  2412. b"7\x00\x00\x95-\x00",
  2413. {
  2414. "isOn": True,
  2415. "mode": "level_3",
  2416. "isAqiValid": False,
  2417. "child_lock": False,
  2418. "speed": 100,
  2419. "aqi_level": "excellent",
  2420. "filter element working time": 405,
  2421. "err_code": 0,
  2422. "sequence_number": 161,
  2423. },
  2424. "7",
  2425. "Air Purifier Table US",
  2426. SwitchbotModel.AIR_PURIFIER_TABLE_US,
  2427. ),
  2428. AdvTestCase(
  2429. b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00',
  2430. b"*\x00\x00\x15\x04\x00",
  2431. {
  2432. "isOn": False,
  2433. "mode": "auto",
  2434. "isAqiValid": False,
  2435. "child_lock": False,
  2436. "speed": 0,
  2437. "aqi_level": "excellent",
  2438. "filter element working time": 15,
  2439. "err_code": 0,
  2440. "sequence_number": 9,
  2441. },
  2442. "*",
  2443. "Air Purifier US",
  2444. SwitchbotModel.AIR_PURIFIER_US,
  2445. ),
  2446. AdvTestCase(
  2447. b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00",
  2448. b"+\x00\x00\x15\x04\x00",
  2449. {
  2450. "isOn": True,
  2451. "mode": "sleep",
  2452. "isAqiValid": False,
  2453. "child_lock": False,
  2454. "speed": 100,
  2455. "aqi_level": "excellent",
  2456. "filter element working time": 60000,
  2457. "err_code": 0,
  2458. "sequence_number": 11,
  2459. },
  2460. "+",
  2461. "Air Purifier JP",
  2462. SwitchbotModel.AIR_PURIFIER_JP,
  2463. ),
  2464. AdvTestCase(
  2465. b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00",
  2466. b"8\x00\x00\x95-\x00",
  2467. {
  2468. "isOn": True,
  2469. "mode": "level_2",
  2470. "isAqiValid": True,
  2471. "child_lock": False,
  2472. "speed": 50,
  2473. "aqi_level": "excellent",
  2474. "filter element working time": 404,
  2475. "err_code": 0,
  2476. "sequence_number": 155,
  2477. },
  2478. "8",
  2479. "Air Purifier Table JP",
  2480. SwitchbotModel.AIR_PURIFIER_TABLE_JP,
  2481. ),
  2482. AdvTestCase(
  2483. b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\xa1\x8c\x800\x01\x95\x00\x00",
  2484. b"8\x00\x00\x95-\x00",
  2485. {
  2486. "isOn": True,
  2487. "mode": "level_1",
  2488. "isAqiValid": True,
  2489. "child_lock": False,
  2490. "speed": 0,
  2491. "aqi_level": "excellent",
  2492. "filter element working time": 405,
  2493. "err_code": 0,
  2494. "sequence_number": 158,
  2495. },
  2496. "8",
  2497. "Air Purifier Table JP",
  2498. SwitchbotModel.AIR_PURIFIER_TABLE_JP,
  2499. ),
  2500. AdvTestCase(
  2501. b"\xcc\x8d\xa2\xa7\xc1\xae\x9e\x05\x8c\x800\x01\x95\x00\x00",
  2502. b"8\x00\x00\x95-\x00",
  2503. {
  2504. "isOn": False,
  2505. "mode": None,
  2506. "isAqiValid": True,
  2507. "child_lock": False,
  2508. "speed": 0,
  2509. "aqi_level": "excellent",
  2510. "filter element working time": 405,
  2511. "err_code": 0,
  2512. "sequence_number": 158,
  2513. },
  2514. "8",
  2515. "Air Purifier Table JP",
  2516. SwitchbotModel.AIR_PURIFIER_TABLE_JP,
  2517. ),
  2518. ],
  2519. )
  2520. def test_air_purifier_active(test_case: AdvTestCase) -> None:
  2521. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2522. adv_data = generate_advertisement_data(
  2523. manufacturer_data={2409: test_case.manufacturer_data},
  2524. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  2525. rssi=-97,
  2526. )
  2527. result = parse_advertisement_data(ble_device, adv_data)
  2528. assert result == SwitchBotAdvertisement(
  2529. address="aa:bb:cc:dd:ee:ff",
  2530. data={
  2531. "rawAdvData": test_case.service_data,
  2532. "data": test_case.data,
  2533. "isEncrypted": False,
  2534. "model": test_case.model,
  2535. "modelFriendlyName": test_case.modelFriendlyName,
  2536. "modelName": test_case.modelName,
  2537. },
  2538. device=ble_device,
  2539. rssi=-97,
  2540. active=True,
  2541. )
  2542. def test_air_purifier_passive() -> None:
  2543. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2544. adv_data = generate_advertisement_data(
  2545. manufacturer_data={
  2546. 2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00"
  2547. },
  2548. rssi=-97,
  2549. )
  2550. result = parse_advertisement_data(
  2551. ble_device, adv_data, SwitchbotModel.AIR_PURIFIER_US
  2552. )
  2553. assert result == SwitchBotAdvertisement(
  2554. address="aa:bb:cc:dd:ee:ff",
  2555. data={
  2556. "rawAdvData": None,
  2557. "data": {
  2558. "isOn": True,
  2559. "mode": "level_3",
  2560. "isAqiValid": False,
  2561. "child_lock": False,
  2562. "speed": 100,
  2563. "aqi_level": "excellent",
  2564. "filter element working time": 405,
  2565. "err_code": 0,
  2566. "sequence_number": 161,
  2567. },
  2568. "isEncrypted": False,
  2569. "model": "*",
  2570. "modelFriendlyName": "Air Purifier US",
  2571. "modelName": SwitchbotModel.AIR_PURIFIER_US,
  2572. },
  2573. device=ble_device,
  2574. rssi=-97,
  2575. active=False,
  2576. )
  2577. def test_air_purifier_with_empty_data() -> None:
  2578. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2579. adv_data = generate_advertisement_data(
  2580. manufacturer_data={2409: None},
  2581. service_data={
  2582. "0000fd3d-0000-1000-8000-00805f9b34fb": b"+\x00\x00\x15\x04\x00",
  2583. },
  2584. rssi=-97,
  2585. )
  2586. result = parse_advertisement_data(ble_device, adv_data)
  2587. assert result == SwitchBotAdvertisement(
  2588. address="aa:bb:cc:dd:ee:ff",
  2589. data={
  2590. "rawAdvData": b"+\x00\x00\x15\x04\x00",
  2591. "data": {},
  2592. "isEncrypted": False,
  2593. "model": "+",
  2594. },
  2595. device=ble_device,
  2596. rssi=-97,
  2597. active=True,
  2598. )
  2599. def test_hub3_active() -> None:
  2600. """Test parsing hub3 with active data."""
  2601. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2602. adv_data = generate_advertisement_data(
  2603. manufacturer_data={2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80"},
  2604. service_data={
  2605. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@"
  2606. },
  2607. rssi=-97,
  2608. )
  2609. result = parse_advertisement_data(ble_device, adv_data)
  2610. assert result == SwitchBotAdvertisement(
  2611. address="aa:bb:cc:dd:ee:ff",
  2612. data={
  2613. "rawAdvData": b"\x00\x00d\x00\x10\xb9@",
  2614. "data": {
  2615. "sequence_number": 0,
  2616. "network_state": 2,
  2617. "sensor_inserted": True,
  2618. "lightLevel": 3,
  2619. "illuminance": 90,
  2620. "temperature_alarm": False,
  2621. "humidity_alarm": False,
  2622. "temp": {"c": 25.3, "f": 77.5},
  2623. "temperature": 25.3,
  2624. "humidity": 52,
  2625. "motion_detected": True,
  2626. },
  2627. "isEncrypted": False,
  2628. "model": b"\x00\x10\xb9@",
  2629. "modelFriendlyName": "Hub3",
  2630. "modelName": SwitchbotModel.HUB3,
  2631. },
  2632. device=ble_device,
  2633. rssi=-97,
  2634. active=True,
  2635. )
  2636. def test_hub3_passive() -> None:
  2637. """Test parsing hub3 with passive data."""
  2638. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2639. adv_data = generate_advertisement_data(
  2640. manufacturer_data={2409: b"\xb0\xe9\xfen^)\x00\xffh&\xd6d\x83\x03\x994\x80"},
  2641. rssi=-97,
  2642. )
  2643. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.HUB3)
  2644. assert result == SwitchBotAdvertisement(
  2645. address="aa:bb:cc:dd:ee:ff",
  2646. data={
  2647. "rawAdvData": None,
  2648. "data": {
  2649. "sequence_number": 0,
  2650. "network_state": 2,
  2651. "sensor_inserted": True,
  2652. "lightLevel": 3,
  2653. "illuminance": 90,
  2654. "temperature_alarm": False,
  2655. "humidity_alarm": False,
  2656. "temp": {"c": 25.3, "f": 77.5},
  2657. "temperature": 25.3,
  2658. "humidity": 52,
  2659. "motion_detected": True,
  2660. },
  2661. "isEncrypted": False,
  2662. "model": b"\x00\x10\xb9@",
  2663. "modelFriendlyName": "Hub3",
  2664. "modelName": SwitchbotModel.HUB3,
  2665. },
  2666. device=ble_device,
  2667. rssi=-97,
  2668. active=False,
  2669. )
  2670. def test_hub3_with_empty_data() -> None:
  2671. """Test parsing hub3 with empty data."""
  2672. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2673. adv_data = generate_advertisement_data(
  2674. manufacturer_data={2409: None},
  2675. service_data={
  2676. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xb9@"
  2677. },
  2678. rssi=-97,
  2679. )
  2680. result = parse_advertisement_data(ble_device, adv_data)
  2681. assert result == SwitchBotAdvertisement(
  2682. address="aa:bb:cc:dd:ee:ff",
  2683. data={
  2684. "rawAdvData": b"\x00\x00d\x00\x10\xb9@",
  2685. "data": {},
  2686. "isEncrypted": False,
  2687. "model": b"\x00\x10\xb9@",
  2688. },
  2689. device=ble_device,
  2690. rssi=-97,
  2691. active=True,
  2692. )
  2693. @pytest.mark.parametrize(
  2694. "test_case",
  2695. [
  2696. AdvTestCase(
  2697. b"\xe9\xd5\x11\xb2kS\x17\x93\x08 ",
  2698. b"-\x80d",
  2699. {
  2700. "sequence_number": 23,
  2701. "battery": 100,
  2702. "calibration": True,
  2703. "status": LockStatus.UNLOCKED,
  2704. "update_from_secondary_lock": False,
  2705. "double_lock_mode": False,
  2706. "unlocked_alarm": False,
  2707. "night_latch": False,
  2708. },
  2709. "-",
  2710. "Lock Lite",
  2711. SwitchbotModel.LOCK_LITE,
  2712. ),
  2713. AdvTestCase(
  2714. b"\xee\xf5\xe6\t\x8f\xe8\x11\x97\x08 ",
  2715. b"o\x80d",
  2716. {
  2717. "sequence_number": 17,
  2718. "battery": 100,
  2719. "calibration": True,
  2720. "status": LockStatus.UNLOCKED,
  2721. "update_from_secondary_lock": False,
  2722. "door_open": True,
  2723. "double_lock_mode": False,
  2724. "unclosed_alarm": False,
  2725. "unlocked_alarm": False,
  2726. "auto_lock_paused": False,
  2727. "night_latch": False,
  2728. },
  2729. "o",
  2730. "Lock",
  2731. SwitchbotModel.LOCK,
  2732. ),
  2733. AdvTestCase(
  2734. b"\xf7a\x07H\xe6\xe8:\x8a\x00d\x00\x00",
  2735. b"$\x80d",
  2736. {
  2737. "sequence_number": 58,
  2738. "battery": 100,
  2739. "calibration": True,
  2740. "status": LockStatus.UNLOCKED,
  2741. "update_from_secondary_lock": False,
  2742. "door_open": False,
  2743. "door_open_from_secondary_lock": False,
  2744. "double_lock_mode": False,
  2745. "is_secondary_lock": False,
  2746. "left_battery_compartment_alarm": 0,
  2747. "right_battery_compartment_alarm": 0,
  2748. "low_temperature_alarm": False,
  2749. "manual_unlock_linkage": False,
  2750. "unclosed_alarm": False,
  2751. "unlocked_alarm": False,
  2752. "auto_lock_paused": False,
  2753. "night_latch": False,
  2754. },
  2755. "$",
  2756. "Lock Pro",
  2757. SwitchbotModel.LOCK_PRO,
  2758. ),
  2759. AdvTestCase(
  2760. b"\xb0\xe9\xfe\xb6j=%\x8204\x00\x04",
  2761. b"\x00\x804\x00\x10\xa5\xb8",
  2762. {
  2763. "sequence_number": 37,
  2764. "battery": 52,
  2765. "calibration": True,
  2766. "status": LockStatus.LOCKED,
  2767. "update_from_secondary_lock": False,
  2768. "door_open": True,
  2769. "door_open_from_secondary_lock": True,
  2770. "double_lock_mode": False,
  2771. "is_secondary_lock": False,
  2772. "manual_unlock_linkage": False,
  2773. "unclosed_alarm": False,
  2774. "unlocked_alarm": False,
  2775. "auto_lock_paused": False,
  2776. "night_latch": False,
  2777. "power_alarm": False,
  2778. "battery_status": 4,
  2779. },
  2780. b"\x00\x10\xa5\xb8",
  2781. "Lock Ultra",
  2782. SwitchbotModel.LOCK_ULTRA,
  2783. ),
  2784. AdvTestCase(
  2785. b"\xb0\xe9\xfe\x6f\xc9\xa6\x0a\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
  2786. b"\x00\x80.\x00\x11i\x08",
  2787. {
  2788. "sequence_number": 10,
  2789. "battery": 46,
  2790. "calibration": False,
  2791. "status": LockStatus.LOCKED,
  2792. "update_from_secondary_lock": False,
  2793. "double_lock_mode": False,
  2794. "unlocked_alarm": False,
  2795. "night_latch": False,
  2796. },
  2797. b"\x00\x11\x69\x08",
  2798. "Lock Vision",
  2799. SwitchbotModel.LOCK_VISION,
  2800. ),
  2801. AdvTestCase(
  2802. b"\xb0\xe9\xfe\xe6\x1aq\x03\x00\x003\x00\x0c\x00\x00\x00\x00",
  2803. b"\x00\x803\x00\x11i\t",
  2804. {
  2805. "sequence_number": 3,
  2806. "battery": 51,
  2807. "calibration": False,
  2808. "status": LockStatus.LOCKED,
  2809. "update_from_secondary_lock": False,
  2810. "door_open": False,
  2811. "door_open_from_secondary_lock": False,
  2812. "double_lock_mode": False,
  2813. "is_secondary_lock": False,
  2814. "manual_unlock_linkage": False,
  2815. "unclosed_alarm": False,
  2816. "unlocked_alarm": False,
  2817. "auto_lock_paused": False,
  2818. "night_latch": False,
  2819. "power_alarm": False,
  2820. "battery_status": 4,
  2821. },
  2822. b"\x00\x11\x69\x09",
  2823. "Lock Vision Pro",
  2824. SwitchbotModel.LOCK_VISION_PRO,
  2825. ),
  2826. AdvTestCase(
  2827. b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
  2828. b"\x00\x80=\x00\x10\xff\x90",
  2829. {
  2830. "sequence_number": 14,
  2831. "battery": 61,
  2832. "calibration": False,
  2833. "status": LockStatus.LOCKED,
  2834. "update_from_secondary_lock": False,
  2835. "door_open": False,
  2836. "door_open_from_secondary_lock": False,
  2837. "double_lock_mode": False,
  2838. "is_secondary_lock": False,
  2839. "left_battery_compartment_alarm": 0,
  2840. "right_battery_compartment_alarm": 0,
  2841. "low_temperature_alarm": False,
  2842. "manual_unlock_linkage": False,
  2843. "unclosed_alarm": False,
  2844. "unlocked_alarm": False,
  2845. "auto_lock_paused": False,
  2846. "night_latch": False,
  2847. },
  2848. b"\x00\x10\xff\x90",
  2849. "Lock Pro Wifi",
  2850. SwitchbotModel.LOCK_PRO_WIFI,
  2851. ),
  2852. AdvTestCase(
  2853. b"\xb0\xe9\xfe\x11\x22\x33\x2a\x88\x18\x64\x00\x91",
  2854. b"\x00\x80\x64\x00\x11\x9f\xb8",
  2855. {
  2856. "sequence_number": 42,
  2857. "battery": 100,
  2858. "calibration": True,
  2859. "status": LockStatus.UNLOCKED,
  2860. "update_from_secondary_lock": False,
  2861. "door_open": True,
  2862. "door_open_from_secondary_lock": False,
  2863. "double_lock_mode": False,
  2864. "is_secondary_lock": False,
  2865. "manual_unlock_linkage": False,
  2866. "unclosed_alarm": True,
  2867. "unlocked_alarm": False,
  2868. "auto_lock_paused": True,
  2869. "night_latch": False,
  2870. "power_alarm": True,
  2871. "battery_status": 1,
  2872. },
  2873. b"\x00\x11\x9f\xb8",
  2874. "Lock Ultra Max",
  2875. SwitchbotModel.LOCK_ULTRA_MAX,
  2876. ),
  2877. AdvTestCase(
  2878. b"\xb0\xe9\xfe\x44\x55\x66\x2b\x10\x00\x50\x00\x00",
  2879. b"\x00\x80\x50\x01\x11\x9f\xb8",
  2880. {
  2881. "sequence_number": 43,
  2882. "battery": 80,
  2883. "calibration": False,
  2884. "status": LockStatus.LOCKING,
  2885. "update_from_secondary_lock": False,
  2886. "door_open": False,
  2887. "door_open_from_secondary_lock": False,
  2888. "double_lock_mode": False,
  2889. "is_secondary_lock": False,
  2890. "manual_unlock_linkage": False,
  2891. "unclosed_alarm": False,
  2892. "unlocked_alarm": False,
  2893. "auto_lock_paused": False,
  2894. "night_latch": False,
  2895. "power_alarm": False,
  2896. "battery_status": 0,
  2897. },
  2898. b"\x01\x11\x9f\xb8",
  2899. "Lock Ultra Max",
  2900. SwitchbotModel.LOCK_ULTRA_MAX,
  2901. ),
  2902. ],
  2903. )
  2904. def test_lock_active(test_case: AdvTestCase) -> None:
  2905. """Test lokc series with active data."""
  2906. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  2907. adv_data = generate_advertisement_data(
  2908. manufacturer_data={2409: test_case.manufacturer_data},
  2909. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  2910. rssi=-97,
  2911. )
  2912. result = parse_advertisement_data(ble_device, adv_data)
  2913. assert result == SwitchBotAdvertisement(
  2914. address="aa:bb:cc:dd:ee:ff",
  2915. data={
  2916. "rawAdvData": test_case.service_data,
  2917. "data": test_case.data,
  2918. "isEncrypted": False,
  2919. "model": test_case.model,
  2920. "modelFriendlyName": test_case.modelFriendlyName,
  2921. "modelName": test_case.modelName,
  2922. },
  2923. device=ble_device,
  2924. rssi=-97,
  2925. active=True,
  2926. )
  2927. @pytest.mark.parametrize(
  2928. "test_case",
  2929. [
  2930. AdvTestCase(
  2931. b"\xe9\xd5\x11\xb2kS\x17\x93\x08 ",
  2932. b"-\x80d",
  2933. {
  2934. "sequence_number": 23,
  2935. "battery": None,
  2936. "calibration": True,
  2937. "status": LockStatus.UNLOCKED,
  2938. "update_from_secondary_lock": False,
  2939. "double_lock_mode": False,
  2940. "unlocked_alarm": False,
  2941. "night_latch": False,
  2942. },
  2943. "-",
  2944. "Lock Lite",
  2945. SwitchbotModel.LOCK_LITE,
  2946. ),
  2947. AdvTestCase(
  2948. b"\xee\xf5\xe6\t\x8f\xe8\x11\x97\x08 ",
  2949. b"o\x80d",
  2950. {
  2951. "sequence_number": 17,
  2952. "battery": None,
  2953. "calibration": True,
  2954. "status": LockStatus.UNLOCKED,
  2955. "update_from_secondary_lock": False,
  2956. "door_open": True,
  2957. "double_lock_mode": False,
  2958. "unclosed_alarm": False,
  2959. "unlocked_alarm": False,
  2960. "auto_lock_paused": False,
  2961. "night_latch": False,
  2962. },
  2963. "o",
  2964. "Lock",
  2965. SwitchbotModel.LOCK,
  2966. ),
  2967. AdvTestCase(
  2968. b"\xf7a\x07H\xe6\xe8:\x8a\x00d\x00\x00",
  2969. b"$\x80d",
  2970. {
  2971. "sequence_number": 58,
  2972. "battery": 100,
  2973. "calibration": True,
  2974. "status": LockStatus.UNLOCKED,
  2975. "update_from_secondary_lock": False,
  2976. "door_open": False,
  2977. "door_open_from_secondary_lock": False,
  2978. "double_lock_mode": False,
  2979. "is_secondary_lock": False,
  2980. "left_battery_compartment_alarm": 0,
  2981. "right_battery_compartment_alarm": 0,
  2982. "low_temperature_alarm": False,
  2983. "manual_unlock_linkage": False,
  2984. "unclosed_alarm": False,
  2985. "unlocked_alarm": False,
  2986. "auto_lock_paused": False,
  2987. "night_latch": False,
  2988. },
  2989. "$",
  2990. "Lock Pro",
  2991. SwitchbotModel.LOCK_PRO,
  2992. ),
  2993. AdvTestCase(
  2994. b"\xb0\xe9\xfe\xb6j=%\x8204\x00\x04",
  2995. b"\x00\x804\x00\x10\xa5\xb8",
  2996. {
  2997. "sequence_number": 37,
  2998. "battery": 52,
  2999. "calibration": True,
  3000. "status": LockStatus.LOCKED,
  3001. "update_from_secondary_lock": False,
  3002. "door_open": True,
  3003. "door_open_from_secondary_lock": True,
  3004. "double_lock_mode": False,
  3005. "is_secondary_lock": False,
  3006. "manual_unlock_linkage": False,
  3007. "unclosed_alarm": False,
  3008. "unlocked_alarm": False,
  3009. "auto_lock_paused": False,
  3010. "night_latch": False,
  3011. "power_alarm": False,
  3012. "battery_status": 4,
  3013. },
  3014. b"\x00\x10\xa5\xb8",
  3015. "Lock Ultra",
  3016. SwitchbotModel.LOCK_ULTRA,
  3017. ),
  3018. AdvTestCase(
  3019. b"\xb0\xe9\xfe\x6f\xc9\xa6\x04\x00\x00\x2e\x00\x0c\x00\x00\x00\x00",
  3020. b"\x00\x80.\x01\x11i\x08",
  3021. {
  3022. "sequence_number": 4,
  3023. "battery": None,
  3024. "calibration": False,
  3025. "status": LockStatus.LOCKED,
  3026. "update_from_secondary_lock": False,
  3027. "double_lock_mode": False,
  3028. "unlocked_alarm": False,
  3029. "night_latch": False,
  3030. },
  3031. b"\x00\x11\x69\x08",
  3032. "Lock Vision",
  3033. SwitchbotModel.LOCK_VISION,
  3034. ),
  3035. AdvTestCase(
  3036. b"\xb0\xe9\xfe\xe6\x1aq\x04\x00\x003\x00\x0c\x00\x00\x00\x00",
  3037. b"\x00\x803\x01\x11i\t",
  3038. {
  3039. "sequence_number": 4,
  3040. "battery": 51,
  3041. "calibration": False,
  3042. "status": LockStatus.LOCKED,
  3043. "update_from_secondary_lock": False,
  3044. "door_open": False,
  3045. "door_open_from_secondary_lock": False,
  3046. "double_lock_mode": False,
  3047. "is_secondary_lock": False,
  3048. "manual_unlock_linkage": False,
  3049. "unclosed_alarm": False,
  3050. "unlocked_alarm": False,
  3051. "auto_lock_paused": False,
  3052. "night_latch": False,
  3053. "power_alarm": False,
  3054. "battery_status": 4,
  3055. },
  3056. b"\x00\x11\x69\x09",
  3057. "Lock Vision Pro",
  3058. SwitchbotModel.LOCK_VISION_PRO,
  3059. ),
  3060. AdvTestCase(
  3061. b"\xb0\xe9\xfe\xb0^\xfe\x0e\x02\x00=\x00\x08",
  3062. b"\x00\x80=\x00\x10\xff\x90",
  3063. {
  3064. "sequence_number": 14,
  3065. "battery": 61,
  3066. "calibration": False,
  3067. "status": LockStatus.LOCKED,
  3068. "update_from_secondary_lock": False,
  3069. "door_open": False,
  3070. "door_open_from_secondary_lock": False,
  3071. "double_lock_mode": False,
  3072. "is_secondary_lock": False,
  3073. "left_battery_compartment_alarm": 0,
  3074. "right_battery_compartment_alarm": 0,
  3075. "low_temperature_alarm": False,
  3076. "manual_unlock_linkage": False,
  3077. "unclosed_alarm": False,
  3078. "unlocked_alarm": False,
  3079. "auto_lock_paused": False,
  3080. "night_latch": False,
  3081. },
  3082. b"\x00\x10\xff\x90",
  3083. "Lock Pro Wifi",
  3084. SwitchbotModel.LOCK_PRO_WIFI,
  3085. ),
  3086. AdvTestCase(
  3087. b"\xb0\xe9\xfe\x11\x22\x33\x2a\x88\x18\x64\x00\x91",
  3088. b"\x00\x80\x64\x00\x11\x9f\xb8",
  3089. {
  3090. "sequence_number": 42,
  3091. "battery": 100,
  3092. "calibration": True,
  3093. "status": LockStatus.UNLOCKED,
  3094. "update_from_secondary_lock": False,
  3095. "door_open": True,
  3096. "door_open_from_secondary_lock": False,
  3097. "double_lock_mode": False,
  3098. "is_secondary_lock": False,
  3099. "manual_unlock_linkage": False,
  3100. "unclosed_alarm": True,
  3101. "unlocked_alarm": False,
  3102. "auto_lock_paused": True,
  3103. "night_latch": False,
  3104. "power_alarm": True,
  3105. "battery_status": 1,
  3106. },
  3107. b"\x00\x11\x9f\xb8",
  3108. "Lock Ultra Max",
  3109. SwitchbotModel.LOCK_ULTRA_MAX,
  3110. ),
  3111. ],
  3112. )
  3113. def test_lock_passive(test_case: AdvTestCase) -> None:
  3114. """Test lokc series with passive data."""
  3115. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3116. adv_data = generate_advertisement_data(
  3117. manufacturer_data={2409: test_case.manufacturer_data},
  3118. rssi=-97,
  3119. )
  3120. result = parse_advertisement_data(ble_device, adv_data, test_case.modelName)
  3121. assert result == SwitchBotAdvertisement(
  3122. address="aa:bb:cc:dd:ee:ff",
  3123. data={
  3124. "rawAdvData": None,
  3125. "data": test_case.data,
  3126. "isEncrypted": False,
  3127. "model": test_case.model,
  3128. "modelFriendlyName": test_case.modelFriendlyName,
  3129. "modelName": test_case.modelName,
  3130. },
  3131. device=ble_device,
  3132. rssi=-97,
  3133. active=False,
  3134. )
  3135. @pytest.mark.parametrize(
  3136. "test_case",
  3137. [
  3138. AdvTestCase(
  3139. None,
  3140. b"-\x80d",
  3141. {},
  3142. "-",
  3143. "Lock Lite",
  3144. SwitchbotModel.LOCK_LITE,
  3145. ),
  3146. AdvTestCase(
  3147. None,
  3148. b"o\x80d",
  3149. {},
  3150. "o",
  3151. "Lock",
  3152. SwitchbotModel.LOCK,
  3153. ),
  3154. AdvTestCase(
  3155. None,
  3156. b"$\x80d",
  3157. {},
  3158. "$",
  3159. "Lock Pro",
  3160. SwitchbotModel.LOCK_PRO,
  3161. ),
  3162. AdvTestCase(
  3163. None,
  3164. b"\x00\x804\x00\x10\xa5\xb8",
  3165. {},
  3166. b"\x00\x10\xa5\xb8",
  3167. "Lock Ultra",
  3168. SwitchbotModel.LOCK_ULTRA,
  3169. ),
  3170. AdvTestCase(
  3171. None,
  3172. b"\x00\x80\x2e\x00\x11\x69\x08",
  3173. {},
  3174. b"\x00\x11\x69\x08",
  3175. "Lock Vision",
  3176. SwitchbotModel.LOCK_VISION,
  3177. ),
  3178. AdvTestCase(
  3179. None,
  3180. b"\x00\x803\x00\x11i\t",
  3181. {},
  3182. b"\x00\x11\x69\x09",
  3183. "Lock Vision Pro",
  3184. SwitchbotModel.LOCK_VISION_PRO,
  3185. ),
  3186. AdvTestCase(
  3187. None,
  3188. b"\x00\x80=\x00\x10\xff\x90",
  3189. {},
  3190. b"\x00\x10\xff\x90",
  3191. "Lock Pro Wifi",
  3192. SwitchbotModel.LOCK_PRO_WIFI,
  3193. ),
  3194. AdvTestCase(
  3195. None,
  3196. b"\x00\x80\x64\x00\x11\x9f\xb8",
  3197. {},
  3198. b"\x00\x11\x9f\xb8",
  3199. "Lock Ultra Max",
  3200. SwitchbotModel.LOCK_ULTRA_MAX,
  3201. ),
  3202. ],
  3203. )
  3204. def test_lock_with_empty_data(test_case: AdvTestCase) -> None:
  3205. """Test lokc series with empty data."""
  3206. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3207. adv_data = generate_advertisement_data(
  3208. manufacturer_data={2409: test_case.manufacturer_data},
  3209. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  3210. rssi=-97,
  3211. )
  3212. result = parse_advertisement_data(ble_device, adv_data, test_case.modelName)
  3213. assert result == SwitchBotAdvertisement(
  3214. address="aa:bb:cc:dd:ee:ff",
  3215. data={
  3216. "rawAdvData": test_case.service_data,
  3217. "data": {},
  3218. "isEncrypted": False,
  3219. "model": test_case.model,
  3220. },
  3221. device=ble_device,
  3222. rssi=-97,
  3223. active=True,
  3224. )
  3225. def test_blind_tilt_active() -> None:
  3226. """Test parsing blind tilt with active data."""
  3227. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3228. adv_data = generate_advertisement_data(
  3229. manufacturer_data={2409: b"\xfc(\\6l\x7f\x0b'\x00\xa1\x84"},
  3230. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"x\x00H"},
  3231. rssi=-97,
  3232. )
  3233. result = parse_advertisement_data(ble_device, adv_data)
  3234. assert result == SwitchBotAdvertisement(
  3235. address="aa:bb:cc:dd:ee:ff",
  3236. data={
  3237. "rawAdvData": b"x\x00H",
  3238. "data": {
  3239. "sequence_number": 11,
  3240. "battery": 72,
  3241. "tilt": 0,
  3242. "inMotion": False,
  3243. "calibration": True,
  3244. "lightLevel": 10,
  3245. },
  3246. "isEncrypted": False,
  3247. "model": "x",
  3248. "modelFriendlyName": "Blind Tilt",
  3249. "modelName": SwitchbotModel.BLIND_TILT,
  3250. },
  3251. device=ble_device,
  3252. rssi=-97,
  3253. active=True,
  3254. )
  3255. def test_blind_tilt_passive() -> None:
  3256. """Test parsing blind tilt with passive data."""
  3257. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3258. adv_data = generate_advertisement_data(
  3259. manufacturer_data={2409: b"\xfc(\\6l\x7f\x0b'\x00\xa1\x84"},
  3260. rssi=-97,
  3261. )
  3262. result = parse_advertisement_data(ble_device, adv_data, SwitchbotModel.BLIND_TILT)
  3263. assert result == SwitchBotAdvertisement(
  3264. address="aa:bb:cc:dd:ee:ff",
  3265. data={
  3266. "rawAdvData": None,
  3267. "data": {
  3268. "sequence_number": 11,
  3269. "battery": None,
  3270. "tilt": 0,
  3271. "inMotion": False,
  3272. "calibration": True,
  3273. "lightLevel": 10,
  3274. },
  3275. "isEncrypted": False,
  3276. "model": "x",
  3277. "modelFriendlyName": "Blind Tilt",
  3278. "modelName": SwitchbotModel.BLIND_TILT,
  3279. },
  3280. device=ble_device,
  3281. rssi=-97,
  3282. active=False,
  3283. )
  3284. def test_blind_tilt_with_empty_data() -> None:
  3285. """Test parsing blind tilt with empty data."""
  3286. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3287. adv_data = generate_advertisement_data(
  3288. manufacturer_data={2409: None},
  3289. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"x\x00H"},
  3290. rssi=-97,
  3291. )
  3292. result = parse_advertisement_data(ble_device, adv_data)
  3293. assert result == SwitchBotAdvertisement(
  3294. address="aa:bb:cc:dd:ee:ff",
  3295. data={
  3296. "rawAdvData": b"x\x00H",
  3297. "data": {},
  3298. "isEncrypted": False,
  3299. "model": "x",
  3300. },
  3301. device=ble_device,
  3302. rssi=-97,
  3303. active=True,
  3304. )
  3305. @pytest.mark.parametrize(
  3306. "test_case",
  3307. [
  3308. AdvTestCase(
  3309. b"\xa0\xa3\xb3,\x9c\xe68\x86\x88\xb5\x99\x12\x10\x1b\x00\x85]",
  3310. b"#\x00\x00\x15\x1c\x00",
  3311. {
  3312. "seq_number": 56,
  3313. "isOn": True,
  3314. "mode": HumidifierMode(6),
  3315. "over_humidify_protection": True,
  3316. "child_lock": False,
  3317. "tank_removed": False,
  3318. "tilted_alert": False,
  3319. "filter_missing": False,
  3320. "is_meter_binded": True,
  3321. "humidity": 53,
  3322. "temperature": 25.1,
  3323. "temp": {"c": 25.1, "f": 77.18},
  3324. "water_level": "medium",
  3325. "filter_run_time": datetime.timedelta(days=1, seconds=10800),
  3326. "filter_alert": False,
  3327. "target_humidity": 93,
  3328. },
  3329. "#",
  3330. "Evaporative Humidifier",
  3331. SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  3332. ),
  3333. AdvTestCase(
  3334. b"\xa0\xa3\xb3,\x9c\xe6H\x86\x80\x7f\xff\xf2\x10\x1d\x00\x874",
  3335. b"#\x00\x00\x15\x1c\x00",
  3336. {
  3337. "seq_number": 72,
  3338. "isOn": True,
  3339. "mode": HumidifierMode(6),
  3340. "over_humidify_protection": True,
  3341. "child_lock": False,
  3342. "tank_removed": False,
  3343. "tilted_alert": False,
  3344. "filter_missing": False,
  3345. "is_meter_binded": False,
  3346. "humidity": None,
  3347. "temperature": None,
  3348. "temp": {"c": None, "f": None},
  3349. "water_level": "medium",
  3350. "filter_run_time": datetime.timedelta(days=1, seconds=18000),
  3351. "filter_alert": False,
  3352. "target_humidity": 52,
  3353. },
  3354. "#",
  3355. "Evaporative Humidifier",
  3356. SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  3357. ),
  3358. AdvTestCase(
  3359. b"\xa0\xa3\xb3,\x9c\xe6H\x86\x80\xff\xff\xf2\x10\x1d\x00\x874",
  3360. b"#\x00\x00\x15\x1c\x00",
  3361. {
  3362. "seq_number": 72,
  3363. "isOn": True,
  3364. "mode": HumidifierMode(6),
  3365. "over_humidify_protection": True,
  3366. "child_lock": False,
  3367. "tank_removed": False,
  3368. "tilted_alert": False,
  3369. "filter_missing": False,
  3370. "is_meter_binded": True,
  3371. "humidity": None,
  3372. "temperature": None,
  3373. "temp": {"c": None, "f": None},
  3374. "water_level": "medium",
  3375. "filter_run_time": datetime.timedelta(days=1, seconds=18000),
  3376. "filter_alert": False,
  3377. "target_humidity": 52,
  3378. },
  3379. "#",
  3380. "Evaporative Humidifier",
  3381. SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  3382. ),
  3383. AdvTestCase(
  3384. b"\xacg\xb2\xcd\xfa\xbe",
  3385. b"e\x80\x00\xf9\x80Bc\x00",
  3386. {
  3387. "isOn": True,
  3388. "level": 128,
  3389. "switchMode": True,
  3390. },
  3391. "e",
  3392. "Humidifier",
  3393. SwitchbotModel.HUMIDIFIER,
  3394. ),
  3395. ],
  3396. )
  3397. def test_humidifer_active(test_case: AdvTestCase) -> None:
  3398. """Test humidifier with active data."""
  3399. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3400. adv_data = generate_advertisement_data(
  3401. manufacturer_data={2409: test_case.manufacturer_data},
  3402. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  3403. rssi=-97,
  3404. )
  3405. result = parse_advertisement_data(ble_device, adv_data)
  3406. assert result == SwitchBotAdvertisement(
  3407. address="aa:bb:cc:dd:ee:ff",
  3408. data={
  3409. "rawAdvData": test_case.service_data,
  3410. "data": test_case.data,
  3411. "isEncrypted": False,
  3412. "model": test_case.model,
  3413. "modelFriendlyName": test_case.modelFriendlyName,
  3414. "modelName": test_case.modelName,
  3415. },
  3416. device=ble_device,
  3417. rssi=-97,
  3418. active=True,
  3419. )
  3420. @pytest.mark.parametrize(
  3421. "test_case",
  3422. [
  3423. AdvTestCase(
  3424. b"\xa0\xa3\xb3,\x9c\xe68\x86\x88\xb5\x99\x12\x10\x1b\x00\x85]",
  3425. b"#\x00\x00\x15\x1c\x00",
  3426. {
  3427. "seq_number": 56,
  3428. "isOn": True,
  3429. "mode": HumidifierMode(6),
  3430. "over_humidify_protection": True,
  3431. "child_lock": False,
  3432. "tank_removed": False,
  3433. "tilted_alert": False,
  3434. "filter_missing": False,
  3435. "is_meter_binded": True,
  3436. "humidity": 53,
  3437. "temperature": 25.1,
  3438. "temp": {"c": 25.1, "f": 77.18},
  3439. "water_level": "medium",
  3440. "filter_run_time": datetime.timedelta(days=1, seconds=10800),
  3441. "filter_alert": False,
  3442. "target_humidity": 93,
  3443. },
  3444. "#",
  3445. "Evaporative Humidifier",
  3446. SwitchbotModel.EVAPORATIVE_HUMIDIFIER,
  3447. ),
  3448. AdvTestCase(
  3449. b"\xacg\xb2\xcd\xfa\xbe",
  3450. b"e\x80\x00\xf9\x80Bc\x00",
  3451. {
  3452. "isOn": None,
  3453. "level": None,
  3454. "switchMode": True,
  3455. },
  3456. "e",
  3457. "Humidifier",
  3458. SwitchbotModel.HUMIDIFIER,
  3459. ),
  3460. ],
  3461. )
  3462. def test_humidifer_passive(test_case: AdvTestCase) -> None:
  3463. """Test humidifier with passive data."""
  3464. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3465. adv_data = generate_advertisement_data(
  3466. manufacturer_data={2409: test_case.manufacturer_data},
  3467. rssi=-97,
  3468. )
  3469. result = parse_advertisement_data(ble_device, adv_data, test_case.modelName)
  3470. assert result == SwitchBotAdvertisement(
  3471. address="aa:bb:cc:dd:ee:ff",
  3472. data={
  3473. "rawAdvData": None,
  3474. "data": test_case.data,
  3475. "isEncrypted": False,
  3476. "model": test_case.model,
  3477. "modelFriendlyName": test_case.modelFriendlyName,
  3478. "modelName": test_case.modelName,
  3479. },
  3480. device=ble_device,
  3481. rssi=-97,
  3482. active=False,
  3483. )
  3484. def test_humidifer_with_empty_data() -> None:
  3485. """Test humidifier with empty data."""
  3486. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3487. adv_data = generate_advertisement_data(
  3488. manufacturer_data={2409: None},
  3489. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"#\x00\x00\x15\x1c\x00"},
  3490. rssi=-97,
  3491. )
  3492. result = parse_advertisement_data(ble_device, adv_data)
  3493. assert result == SwitchBotAdvertisement(
  3494. address="aa:bb:cc:dd:ee:ff",
  3495. data={
  3496. "rawAdvData": b"#\x00\x00\x15\x1c\x00",
  3497. "data": {},
  3498. "isEncrypted": False,
  3499. "model": "#",
  3500. },
  3501. device=ble_device,
  3502. rssi=-97,
  3503. active=True,
  3504. )
  3505. @pytest.mark.parametrize(
  3506. "test_case",
  3507. [
  3508. AdvTestCase(
  3509. b"\xc0N0\xdd\xb9\xf2\x8a\xc1\x00\x00\x00\x00\x00F\x00\x00",
  3510. b"=\x00\x00\x00",
  3511. {
  3512. 1: {
  3513. "isOn": True,
  3514. "sequence_number": 138,
  3515. "switchMode": True,
  3516. "power": 0.0,
  3517. },
  3518. 2: {
  3519. "isOn": True,
  3520. "sequence_number": 138,
  3521. "switchMode": True,
  3522. "power": 70.0,
  3523. },
  3524. "sequence_number": 138,
  3525. },
  3526. "=",
  3527. "Relay Switch 2PM",
  3528. SwitchbotModel.RELAY_SWITCH_2PM,
  3529. ),
  3530. AdvTestCase(
  3531. b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00",
  3532. b";\x00\x00\x00",
  3533. {
  3534. "isOn": True,
  3535. "sequence_number": 71,
  3536. "switchMode": True,
  3537. },
  3538. ";",
  3539. "Relay Switch 1",
  3540. SwitchbotModel.RELAY_SWITCH_1,
  3541. ),
  3542. AdvTestCase(
  3543. b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00",
  3544. b"<\x00\x00\x00",
  3545. {
  3546. "isOn": True,
  3547. "sequence_number": 71,
  3548. "switchMode": True,
  3549. "power": 49.0,
  3550. },
  3551. "<",
  3552. "Relay Switch 1PM",
  3553. SwitchbotModel.RELAY_SWITCH_1PM,
  3554. ),
  3555. AdvTestCase(
  3556. b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00",
  3557. b">\x00\x00\x00",
  3558. {
  3559. "door_open": True,
  3560. "isOn": False,
  3561. "sequence_number": 15,
  3562. "switchMode": True,
  3563. },
  3564. ">",
  3565. "Garage Door Opener",
  3566. SwitchbotModel.GARAGE_DOOR_OPENER,
  3567. ),
  3568. AdvTestCase(
  3569. b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00',
  3570. b"\x00\x00\x00\x00\x10\xd0\xb1",
  3571. {
  3572. "sequence_number": 133,
  3573. "isOn": True,
  3574. "brightness": 30,
  3575. "delay": False,
  3576. "network_state": 2,
  3577. "color_mode": 2,
  3578. "cw": 4753,
  3579. },
  3580. b"\x00\x10\xd0\xb1",
  3581. "Strip Light 3",
  3582. SwitchbotModel.STRIP_LIGHT_3,
  3583. ),
  3584. AdvTestCase(
  3585. b'\xa0\x85\xe3e,\x06P\xaa"\xd4\x00\x00\x00\x00\x00\x00\r\x93\x00',
  3586. b"\x00\x00\x00\x00\x10\xd0\xb0",
  3587. {
  3588. "sequence_number": 80,
  3589. "isOn": True,
  3590. "brightness": 42,
  3591. "delay": False,
  3592. "network_state": 2,
  3593. "color_mode": 2,
  3594. "cw": 3475,
  3595. },
  3596. b"\x00\x10\xd0\xb0",
  3597. "Floor Lamp",
  3598. SwitchbotModel.FLOOR_LAMP,
  3599. ),
  3600. AdvTestCase(
  3601. b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
  3602. b"\x00\x00\x00\x00\x11\x22\xb8",
  3603. {
  3604. "brightness": 48,
  3605. "delay": False,
  3606. "isOn": True,
  3607. "network_state": 2,
  3608. "sequence_number": 10,
  3609. },
  3610. b"\x00\x11\x22\xb8",
  3611. "Candle Warmer Lamp",
  3612. SwitchbotModel.CANDLE_WARMER_LAMP,
  3613. ),
  3614. AdvTestCase(
  3615. b"\xef\xfe\xfb\x9d\x10\xfe\n\x01\x18\xf3$",
  3616. b"q\x00",
  3617. {
  3618. "brightness": 1,
  3619. "color_mode": 1,
  3620. "cw": 6387,
  3621. "isOn": False,
  3622. "sequence_number": 10,
  3623. },
  3624. "q",
  3625. "Ceiling Light",
  3626. SwitchbotModel.CEILING_LIGHT,
  3627. ),
  3628. AdvTestCase(
  3629. b'|,g\xc8\x15&jR"l\x00\x00\x00\x00\x00\x00',
  3630. b"r\x00d",
  3631. {
  3632. "brightness": 82,
  3633. "color_mode": 2,
  3634. "delay": False,
  3635. "isOn": False,
  3636. "sequence_number": 106,
  3637. "network_state": 2,
  3638. },
  3639. "r",
  3640. "Light Strip",
  3641. SwitchbotModel.LIGHT_STRIP,
  3642. ),
  3643. AdvTestCase(
  3644. b"@L\xca\xa7_\x12\x02\x81\x12\x00\x00",
  3645. b"u\x00d",
  3646. {
  3647. "brightness": 1,
  3648. "color_mode": 2,
  3649. "delay": False,
  3650. "isOn": True,
  3651. "loop_index": 0,
  3652. "preset": False,
  3653. "sequence_number": 2,
  3654. "speed": 0,
  3655. },
  3656. "u",
  3657. "Color Bulb",
  3658. SwitchbotModel.COLOR_BULB,
  3659. ),
  3660. AdvTestCase(
  3661. b"\x94\xa9\x90T\x85^?\xa1\x00\x00\x04\xe6\x00\x00\x00\x00",
  3662. b"?\x00\x00\x00",
  3663. {
  3664. "isOn": True,
  3665. "power": 1254.0,
  3666. "sequence_number": 63,
  3667. "switchMode": True,
  3668. },
  3669. "?",
  3670. "Plug Mini (EU)",
  3671. SwitchbotModel.PLUG_MINI_EU,
  3672. ),
  3673. AdvTestCase(
  3674. b'\xdc\x06u\xa6\xfb\xb2y\x9e"\x00\x11\xb8\x00',
  3675. b"\x00\x00\x00\x00\x10\xd0\xb4",
  3676. {
  3677. "sequence_number": 121,
  3678. "isOn": True,
  3679. "brightness": 30,
  3680. "delay": False,
  3681. "network_state": 2,
  3682. "color_mode": 2,
  3683. "cw": 4536,
  3684. },
  3685. b"\x00\x10\xd0\xb4",
  3686. "RGBICWW Floor Lamp",
  3687. SwitchbotModel.RGBICWW_FLOOR_LAMP,
  3688. ),
  3689. AdvTestCase(
  3690. b'(7/L\x94\xb2\x0c\x9e"\x00\x11:\x00',
  3691. b"\x00\x00\x00\x00\x10\xd0\xb3",
  3692. {
  3693. "sequence_number": 12,
  3694. "isOn": True,
  3695. "brightness": 30,
  3696. "delay": False,
  3697. "network_state": 2,
  3698. "color_mode": 2,
  3699. "cw": 4410,
  3700. },
  3701. b"\x00\x10\xd0\xb3",
  3702. "RGBICWW Strip Light",
  3703. SwitchbotModel.RGBICWW_STRIP_LIGHT,
  3704. ),
  3705. AdvTestCase(
  3706. b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00',
  3707. b"\x00\x00\x00\x00\x10\xd0\xb7",
  3708. {
  3709. "sequence_number": 133,
  3710. "isOn": True,
  3711. "brightness": 30,
  3712. "delay": False,
  3713. "network_state": 2,
  3714. "color_mode": 2,
  3715. "cw": 0,
  3716. },
  3717. b"\x00\x10\xd0\xb7",
  3718. "Permanent Outdoor Light",
  3719. SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
  3720. ),
  3721. AdvTestCase(
  3722. b"@L\xca!pz/\x8b'\x00\x11:\x00",
  3723. b"\x00\x00\x00\x00\x10\xd0\xb6",
  3724. {
  3725. "sequence_number": 47,
  3726. "isOn": True,
  3727. "brightness": 11,
  3728. "delay": False,
  3729. "network_state": 2,
  3730. "color_mode": 7,
  3731. },
  3732. b"\x00\x10\xd0\xb6",
  3733. "RGBIC Neon Rope Light",
  3734. SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
  3735. ),
  3736. AdvTestCase(
  3737. b"@L\xca!pz/\x8b'\x00\x11:\x00",
  3738. b"\x00\x00\x00\x00\x10\xd0\xb5",
  3739. {
  3740. "sequence_number": 47,
  3741. "isOn": True,
  3742. "brightness": 11,
  3743. "delay": False,
  3744. "network_state": 2,
  3745. "color_mode": 7,
  3746. },
  3747. b"\x00\x10\xd0\xb5",
  3748. "RGBIC Neon Wire Rope Light",
  3749. SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
  3750. ),
  3751. AdvTestCase(
  3752. b'(7/L\x94\xb2\x0c\x9e"\x00\x11:\x00\xa0',
  3753. b"\x00\x00\x00\x00\x11\xbb\x10",
  3754. {
  3755. "sequence_number": 12,
  3756. "isOn": True,
  3757. "brightness": 30,
  3758. "delay": False,
  3759. "network_state": 2,
  3760. "color_mode": 2,
  3761. "cw": 4410,
  3762. "main_isOn": True,
  3763. "main_brightness": 32,
  3764. },
  3765. b"\x00\x11\xbb\x10",
  3766. "RGBICWW Ceiling Light",
  3767. SwitchbotModel.RGBICWW_CEILING_LIGHT,
  3768. ),
  3769. AdvTestCase(
  3770. b"\xb0\xe9\xfe\xe4\xbf\xd8\x0b\x01\x11f\x00\x16M\x15",
  3771. b"\x00\x00M\x00\x10\xfb\xa8",
  3772. {
  3773. "battery": 77,
  3774. "mqtt_connected": True,
  3775. "sequence_number": 11,
  3776. "soc_version": "1.1.102",
  3777. "step": 6,
  3778. "work_status": 21,
  3779. },
  3780. b"\x00\x10\xfb\xa8",
  3781. "K11+ Vacuum",
  3782. SwitchbotModel.K11_VACUUM,
  3783. ),
  3784. AdvTestCase(
  3785. b"\xb0\xe9\xfe\x848\x05\x06\x14\x05\x99-\x00\x00\x00\x00\xd4\x00\x0c\x04\x00",
  3786. b"\x00 _\x00\x10\xf3\xd8@",
  3787. {
  3788. "battery": 20,
  3789. "humidity": 45,
  3790. "sequence_number": 6,
  3791. "humidity_alarm": 0,
  3792. "isOn": False,
  3793. "is_light": True,
  3794. "motion_detected": True,
  3795. "temp_alarm": 0,
  3796. "temperature": 25.5,
  3797. "on_keystate": 0,
  3798. "off_keystate": 0,
  3799. "on_keystate_mode": 0,
  3800. "on_keystate_counter": 0,
  3801. "off_keystate_mode": 0,
  3802. "off_keystate_counter": 0,
  3803. },
  3804. b"\x00\x10\xf3\xd8",
  3805. "Climate Panel",
  3806. SwitchbotModel.CLIMATE_PANEL,
  3807. ),
  3808. AdvTestCase(
  3809. b"\xb0\xe9\xfe\x848\x05\x13\x14\t\x99-\x00\x00\x00\x00\xd2\x01\x14\x04\x00",
  3810. b"\x00 _\x00\x10\xf3\xd8@",
  3811. {
  3812. "battery": 20,
  3813. "humidity": 45,
  3814. "sequence_number": 19,
  3815. "humidity_alarm": 0,
  3816. "isOn": False,
  3817. "is_light": False,
  3818. "motion_detected": True,
  3819. "temp_alarm": 0,
  3820. "temperature": 25.9,
  3821. "on_keystate": 0,
  3822. "off_keystate": 0,
  3823. "on_keystate_mode": 0,
  3824. "on_keystate_counter": 0,
  3825. "off_keystate_mode": 0,
  3826. "off_keystate_counter": 0,
  3827. },
  3828. b"\x00\x10\xf3\xd8",
  3829. "Climate Panel",
  3830. SwitchbotModel.CLIMATE_PANEL,
  3831. ),
  3832. AdvTestCase(
  3833. b"\xb0\xe9\xfe\x84\x38\x05\x06\x14\x05\x99\x2d\x00\x00\x25\x62\xd4\x00\x0c\x04\x00",
  3834. b"\x00 _\x00\x10\xf3\xd8@",
  3835. {
  3836. "battery": 20,
  3837. "humidity": 45,
  3838. "sequence_number": 6,
  3839. "humidity_alarm": 0,
  3840. "isOn": False,
  3841. "is_light": True,
  3842. "motion_detected": True,
  3843. "temp_alarm": 0,
  3844. "temperature": 25.5,
  3845. "on_keystate": 37,
  3846. "off_keystate": 98,
  3847. "on_keystate_mode": 1,
  3848. "on_keystate_counter": 5,
  3849. "off_keystate_mode": 3,
  3850. "off_keystate_counter": 2,
  3851. },
  3852. b"\x00\x10\xf3\xd8",
  3853. "Climate Panel",
  3854. SwitchbotModel.CLIMATE_PANEL,
  3855. ),
  3856. AdvTestCase(
  3857. b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00",
  3858. b"\x00 d\x00\x116@",
  3859. {
  3860. "battery": 100,
  3861. "door_open": False,
  3862. "fault_code": 0,
  3863. "isOn": True,
  3864. "last_mode": "comfort",
  3865. "mode": "manual",
  3866. "sequence_number": 54,
  3867. "need_update_temp": False,
  3868. "restarted": False,
  3869. "target_temperature": 35.0,
  3870. "temperature": 28.0,
  3871. },
  3872. b"\x00\x116@",
  3873. "Smart Thermostat Radiator",
  3874. SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
  3875. ),
  3876. AdvTestCase(
  3877. b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03",
  3878. b"\x00\x00d\x00\x10\xe0P",
  3879. {
  3880. "battery": 100,
  3881. "mqtt_connected": False,
  3882. "sequence_number": 58,
  3883. "soc_version": "1.1.030",
  3884. "step": 0,
  3885. "work_status": 3,
  3886. },
  3887. b"\x00\x10\xe0P",
  3888. "S20 Vacuum",
  3889. SwitchbotModel.S20_VACUUM,
  3890. ),
  3891. AdvTestCase(
  3892. b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c",
  3893. b"\x00 d\x00\x10\xcc\xc8",
  3894. {
  3895. "adaptive_state": True,
  3896. "battery": 100,
  3897. "battery_range": ">=60%",
  3898. "duration": 1110,
  3899. "led_state": True,
  3900. "lightLevel": 12,
  3901. "motion_detected": True,
  3902. "sequence_number": 6,
  3903. "trigger_flag": 0,
  3904. },
  3905. b"\x00\x10\xcc\xc8",
  3906. "Presence Sensor",
  3907. SwitchbotModel.PRESENCE_SENSOR,
  3908. ),
  3909. AdvTestCase(
  3910. b"\xb0\xe9\xfeR\xdd\x84\x06d\x08\x97,\x00\x05",
  3911. b"\x14\x00d",
  3912. {
  3913. "battery": 100,
  3914. "fahrenheit": False,
  3915. "humidity": 44,
  3916. "temp": {"c": 23.8, "f": 74.84},
  3917. "temperature": 23.8,
  3918. },
  3919. b"\x14",
  3920. "Meter Pro",
  3921. SwitchbotModel.METER_PRO,
  3922. ),
  3923. AdvTestCase(
  3924. b"\xb0\xe9\xfe\xe2\xfa8\x157\x03\x08",
  3925. b"\x00\x007\x01\x11>\x10",
  3926. {
  3927. "battery": 55,
  3928. "battery_charging": False,
  3929. "display_mode": 1,
  3930. "display_size": 0,
  3931. "image_index": 3,
  3932. "last_network_status": 0,
  3933. "sequence_number": 21,
  3934. },
  3935. b"\x01\x11>\x10",
  3936. "Art Frame",
  3937. SwitchbotModel.ART_FRAME,
  3938. ),
  3939. AdvTestCase(
  3940. b"\xb0\xe9\xfe\xe5\x04\x1e\xac\xdf\x00\x00\x00\x00\x00\x02",
  3941. b"\x00\x00_\x01\x11\x03x",
  3942. {
  3943. "battery": 95,
  3944. "battery_charging": True,
  3945. "doorbell": False,
  3946. "doorbell_seq": 0,
  3947. "duress_alarm": False,
  3948. "high_temperature": False,
  3949. "lockout_alarm": False,
  3950. "low_temperature": False,
  3951. "pir_triggered_level": 2,
  3952. "sequence_number": 172,
  3953. "tamper_alarm": False,
  3954. },
  3955. b"\x01\x11\x03x",
  3956. "Keypad Vision",
  3957. SwitchbotModel.KEYPAD_VISION,
  3958. ),
  3959. AdvTestCase(
  3960. b"\xb0\xe9\xfe\xde\xb6\x8c+`\x00\x00\x00\x00\x00\x002",
  3961. b"\x00\x00`\x01\x11Q\x98",
  3962. {
  3963. "battery": 96,
  3964. "battery_charging": False,
  3965. "doorbell": False,
  3966. "doorbell_seq": 0,
  3967. "duress_alarm": False,
  3968. "high_temperature": False,
  3969. "lockout_alarm": False,
  3970. "low_temperature": False,
  3971. "radar_triggered_distance": 0,
  3972. "radar_triggered_level": 0,
  3973. "sequence_number": 43,
  3974. "tamper_alarm": False,
  3975. },
  3976. b"\x01\x11Q\x98",
  3977. "Keypad Vision Pro",
  3978. SwitchbotModel.KEYPAD_VISION_PRO,
  3979. ),
  3980. ],
  3981. )
  3982. def test_adv_active(test_case: AdvTestCase) -> None:
  3983. """Test with active data."""
  3984. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  3985. adv_data = generate_advertisement_data(
  3986. manufacturer_data={2409: test_case.manufacturer_data},
  3987. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  3988. rssi=-97,
  3989. )
  3990. result = parse_advertisement_data(ble_device, adv_data)
  3991. assert result == SwitchBotAdvertisement(
  3992. address="aa:bb:cc:dd:ee:ff",
  3993. data={
  3994. "rawAdvData": test_case.service_data,
  3995. "data": test_case.data,
  3996. "isEncrypted": False,
  3997. "model": test_case.model,
  3998. "modelFriendlyName": test_case.modelFriendlyName,
  3999. "modelName": test_case.modelName,
  4000. },
  4001. device=ble_device,
  4002. rssi=-97,
  4003. active=True,
  4004. )
  4005. @pytest.mark.parametrize(
  4006. "test_case",
  4007. [
  4008. AdvTestCase(
  4009. b"\xc0N0\xdd\xb9\xf2\x8a\xc1\x00\x00\x00\x00\x00F\x00\x00",
  4010. b"=\x00\x00\x00",
  4011. {
  4012. 1: {
  4013. "isOn": True,
  4014. "sequence_number": 138,
  4015. "switchMode": True,
  4016. "power": 0.0,
  4017. },
  4018. 2: {
  4019. "isOn": True,
  4020. "sequence_number": 138,
  4021. "switchMode": True,
  4022. "power": 70.0,
  4023. },
  4024. "sequence_number": 138,
  4025. },
  4026. "=",
  4027. "Relay Switch 2PM",
  4028. SwitchbotModel.RELAY_SWITCH_2PM,
  4029. ),
  4030. AdvTestCase(
  4031. b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00",
  4032. b";\x00\x00\x00",
  4033. {
  4034. "isOn": True,
  4035. "sequence_number": 71,
  4036. "switchMode": True,
  4037. },
  4038. ";",
  4039. "Relay Switch 1",
  4040. SwitchbotModel.RELAY_SWITCH_1,
  4041. ),
  4042. AdvTestCase(
  4043. b"$X|\x0866G\x81\x00\x00\x001\x00\x00\x00\x00",
  4044. b"<\x00\x00\x00",
  4045. {
  4046. "isOn": True,
  4047. "sequence_number": 71,
  4048. "switchMode": True,
  4049. "power": 49.0,
  4050. },
  4051. "<",
  4052. "Relay Switch 1PM",
  4053. SwitchbotModel.RELAY_SWITCH_1PM,
  4054. ),
  4055. AdvTestCase(
  4056. b"$X|\x05BN\x0f\x00\x00\x03\x00\x00\x00\x00\x00\x00",
  4057. b">\x00\x00\x00",
  4058. {
  4059. "door_open": True,
  4060. "isOn": False,
  4061. "sequence_number": 15,
  4062. "switchMode": True,
  4063. },
  4064. ">",
  4065. "Garage Door Opener",
  4066. SwitchbotModel.GARAGE_DOOR_OPENER,
  4067. ),
  4068. AdvTestCase(
  4069. b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00',
  4070. None,
  4071. {
  4072. "sequence_number": 133,
  4073. "isOn": True,
  4074. "brightness": 30,
  4075. "delay": False,
  4076. "network_state": 2,
  4077. "color_mode": 2,
  4078. "cw": 4753,
  4079. },
  4080. b"\x00\x10\xd0\xb1",
  4081. "Strip Light 3",
  4082. SwitchbotModel.STRIP_LIGHT_3,
  4083. ),
  4084. AdvTestCase(
  4085. b'\xa0\x85\xe3e,\x06P\xaa"\xd4\x00\x00\x00\x00\x00\x00\r\x93\x00',
  4086. None,
  4087. {
  4088. "sequence_number": 80,
  4089. "isOn": True,
  4090. "brightness": 42,
  4091. "delay": False,
  4092. "network_state": 2,
  4093. "color_mode": 2,
  4094. "cw": 3475,
  4095. },
  4096. b"\x00\x10\xd0\xb0",
  4097. "Floor Lamp",
  4098. SwitchbotModel.FLOOR_LAMP,
  4099. ),
  4100. AdvTestCase(
  4101. b'|,g\xc8\x15&jR"l\x00\x00\x00\x00\x00\x00',
  4102. None,
  4103. {
  4104. "brightness": 82,
  4105. "color_mode": 2,
  4106. "delay": False,
  4107. "isOn": False,
  4108. "sequence_number": 106,
  4109. "network_state": 2,
  4110. },
  4111. "r",
  4112. "Light Strip",
  4113. SwitchbotModel.LIGHT_STRIP,
  4114. ),
  4115. AdvTestCase(
  4116. b"@L\xca\xa7_\x12\x02\x81\x12\x00\x00",
  4117. None,
  4118. {
  4119. "brightness": 1,
  4120. "color_mode": 2,
  4121. "delay": False,
  4122. "isOn": True,
  4123. "loop_index": 0,
  4124. "preset": False,
  4125. "sequence_number": 2,
  4126. "speed": 0,
  4127. },
  4128. "u",
  4129. "Color Bulb",
  4130. SwitchbotModel.COLOR_BULB,
  4131. ),
  4132. AdvTestCase(
  4133. b"\x94\xa9\x90T\x85^?\xa1\x00\x00\x04\xe6\x00\x00\x00\x00",
  4134. None,
  4135. {
  4136. "isOn": True,
  4137. "power": 1254.0,
  4138. "sequence_number": 63,
  4139. "switchMode": True,
  4140. },
  4141. "?",
  4142. "Plug Mini (EU)",
  4143. SwitchbotModel.PLUG_MINI_EU,
  4144. ),
  4145. AdvTestCase(
  4146. b'\xdc\x06u\xa6\xfb\xb2y\x9e"\x00\x11\xb8\x00',
  4147. None,
  4148. {
  4149. "sequence_number": 121,
  4150. "isOn": True,
  4151. "brightness": 30,
  4152. "delay": False,
  4153. "network_state": 2,
  4154. "color_mode": 2,
  4155. "cw": 4536,
  4156. },
  4157. b"\x00\x10\xd0\xb4",
  4158. "RGBICWW Floor Lamp",
  4159. SwitchbotModel.RGBICWW_FLOOR_LAMP,
  4160. ),
  4161. AdvTestCase(
  4162. b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
  4163. None,
  4164. {
  4165. "brightness": 48,
  4166. "delay": False,
  4167. "isOn": True,
  4168. "network_state": 2,
  4169. "sequence_number": 10,
  4170. },
  4171. b"\x00\x11\x22\xb8",
  4172. "Candle Warmer Lamp",
  4173. SwitchbotModel.CANDLE_WARMER_LAMP,
  4174. ),
  4175. AdvTestCase(
  4176. b'(7/L\x94\xb2\x0c\x9e"\x00\x11:\x00',
  4177. None,
  4178. {
  4179. "sequence_number": 12,
  4180. "isOn": True,
  4181. "brightness": 30,
  4182. "delay": False,
  4183. "network_state": 2,
  4184. "color_mode": 2,
  4185. "cw": 4410,
  4186. },
  4187. b"\x00\x10\xd0\xb3",
  4188. "RGBICWW Strip Light",
  4189. SwitchbotModel.RGBICWW_STRIP_LIGHT,
  4190. ),
  4191. AdvTestCase(
  4192. b'\xc0N0\xe0U\x9a\x85\x9e"\xd0\x00\x00\x00\x00\x00\x00\x12\x91\x00',
  4193. None,
  4194. {
  4195. "sequence_number": 133,
  4196. "isOn": True,
  4197. "brightness": 30,
  4198. "delay": False,
  4199. "network_state": 2,
  4200. "color_mode": 2,
  4201. "cw": 0,
  4202. },
  4203. b"\x00\x10\xd0\xb7",
  4204. "Permanent Outdoor Light",
  4205. SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
  4206. ),
  4207. AdvTestCase(
  4208. b"@L\xca!pz/\x8b'\x00\x11:\x00",
  4209. None,
  4210. {
  4211. "sequence_number": 47,
  4212. "isOn": True,
  4213. "brightness": 11,
  4214. "delay": False,
  4215. "network_state": 2,
  4216. "color_mode": 7,
  4217. },
  4218. b"\x00\x10\xd0\xb6",
  4219. "RGBIC Neon Rope Light",
  4220. SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
  4221. ),
  4222. AdvTestCase(
  4223. b"@L\xca!pz/\x8b'\x00\x11:\x00",
  4224. None,
  4225. {
  4226. "sequence_number": 47,
  4227. "isOn": True,
  4228. "brightness": 11,
  4229. "delay": False,
  4230. "network_state": 2,
  4231. "color_mode": 7,
  4232. },
  4233. b"\x00\x10\xd0\xb5",
  4234. "RGBIC Neon Wire Rope Light",
  4235. SwitchbotModel.RGBIC_NEON_WIRE_ROPE_LIGHT,
  4236. ),
  4237. AdvTestCase(
  4238. b'(7/L\x94\xb2\x0c\x9e"\x00\x11:\x00\xa0',
  4239. None,
  4240. {
  4241. "sequence_number": 12,
  4242. "isOn": True,
  4243. "brightness": 30,
  4244. "delay": False,
  4245. "network_state": 2,
  4246. "color_mode": 2,
  4247. "cw": 4410,
  4248. "main_isOn": True,
  4249. "main_brightness": 32,
  4250. },
  4251. b"\x00\x11\xbb\x10",
  4252. "RGBICWW Ceiling Light",
  4253. SwitchbotModel.RGBICWW_CEILING_LIGHT,
  4254. ),
  4255. AdvTestCase(
  4256. b"\xb0\xe9\xfe\xe4\xbf\xd8\x0b\x01\x11f\x00\x16M\x15",
  4257. None,
  4258. {
  4259. "battery": 77,
  4260. "mqtt_connected": True,
  4261. "sequence_number": 11,
  4262. "soc_version": "1.1.102",
  4263. "step": 6,
  4264. "work_status": 21,
  4265. },
  4266. b"\x00\x10\xfb\xa8",
  4267. "K11+ Vacuum",
  4268. SwitchbotModel.K11_VACUUM,
  4269. ),
  4270. AdvTestCase(
  4271. b"\xb0\xe9\xfe\x8e\x98Oi_\x06\x9a,\x00\x00\x00\x00\xe4\x00\x08\x04\x00\x01\x00\x00",
  4272. None,
  4273. {
  4274. "battery": 95,
  4275. "humidity": 44,
  4276. "sequence_number": 105,
  4277. "humidity_alarm": 0,
  4278. "isOn": False,
  4279. "is_light": True,
  4280. "motion_detected": True,
  4281. "temp_alarm": 0,
  4282. "temperature": 26.6,
  4283. "on_keystate": 0,
  4284. "off_keystate": 0,
  4285. "on_keystate_mode": 0,
  4286. "on_keystate_counter": 0,
  4287. "off_keystate_mode": 0,
  4288. "off_keystate_counter": 0,
  4289. },
  4290. b"\x00\x10\xf3\xd8",
  4291. "Climate Panel",
  4292. SwitchbotModel.CLIMATE_PANEL,
  4293. ),
  4294. AdvTestCase(
  4295. b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00",
  4296. None,
  4297. {
  4298. "battery": 100,
  4299. "door_open": False,
  4300. "fault_code": 0,
  4301. "isOn": True,
  4302. "last_mode": "comfort",
  4303. "mode": "manual",
  4304. "sequence_number": 54,
  4305. "need_update_temp": False,
  4306. "restarted": False,
  4307. "target_temperature": 35.0,
  4308. "temperature": 28.0,
  4309. },
  4310. b"\x00\x116@",
  4311. "Smart Thermostat Radiator",
  4312. SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
  4313. ),
  4314. AdvTestCase(
  4315. b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03",
  4316. None,
  4317. {
  4318. "battery": 100,
  4319. "mqtt_connected": False,
  4320. "sequence_number": 58,
  4321. "soc_version": "1.1.030",
  4322. "step": 0,
  4323. "work_status": 3,
  4324. },
  4325. b"\x00\x10\xe0P",
  4326. "S20 Vacuum",
  4327. SwitchbotModel.S20_VACUUM,
  4328. ),
  4329. AdvTestCase(
  4330. b"\xb0\xe9\xfelf\xaa\x06\xcc\x04V\x00\x8c",
  4331. None,
  4332. {
  4333. "adaptive_state": True,
  4334. "battery_range": ">=60%",
  4335. "duration": 1110,
  4336. "led_state": True,
  4337. "lightLevel": 12,
  4338. "motion_detected": True,
  4339. "sequence_number": 6,
  4340. "trigger_flag": 0,
  4341. },
  4342. b"\x00\x10\xcc\xc8",
  4343. "Presence Sensor",
  4344. SwitchbotModel.PRESENCE_SENSOR,
  4345. ),
  4346. AdvTestCase(
  4347. b"\xb0\xe9\xfe\xe2\xfa8\x157\x03\x08",
  4348. None,
  4349. {
  4350. "battery": 55,
  4351. "battery_charging": False,
  4352. "display_mode": 1,
  4353. "display_size": 0,
  4354. "image_index": 3,
  4355. "last_network_status": 0,
  4356. "sequence_number": 21,
  4357. },
  4358. b"\x00\x11>\x10",
  4359. "Art Frame",
  4360. SwitchbotModel.ART_FRAME,
  4361. ),
  4362. AdvTestCase(
  4363. b"\xb0\xe9\xfe\xe5\x04\x1e\xac\xdf\x00\x00\x00\x00\x00\x02",
  4364. None,
  4365. {
  4366. "battery": 95,
  4367. "battery_charging": True,
  4368. "doorbell": False,
  4369. "doorbell_seq": 0,
  4370. "duress_alarm": False,
  4371. "high_temperature": False,
  4372. "lockout_alarm": False,
  4373. "low_temperature": False,
  4374. "pir_triggered_level": 2,
  4375. "sequence_number": 172,
  4376. "tamper_alarm": False,
  4377. },
  4378. b"\x00\x11\x03x",
  4379. "Keypad Vision",
  4380. SwitchbotModel.KEYPAD_VISION,
  4381. ),
  4382. AdvTestCase(
  4383. b"\xb0\xe9\xfe\xde\xb6\x8c+`\x00\x00\x00\x00\x00\x002",
  4384. None,
  4385. {
  4386. "battery": 96,
  4387. "battery_charging": False,
  4388. "doorbell": False,
  4389. "doorbell_seq": 0,
  4390. "duress_alarm": False,
  4391. "high_temperature": False,
  4392. "lockout_alarm": False,
  4393. "low_temperature": False,
  4394. "radar_triggered_distance": 0,
  4395. "radar_triggered_level": 0,
  4396. "sequence_number": 43,
  4397. "tamper_alarm": False,
  4398. },
  4399. b"\x00\x11Q\x98",
  4400. "Keypad Vision Pro",
  4401. SwitchbotModel.KEYPAD_VISION_PRO,
  4402. ),
  4403. ],
  4404. )
  4405. def test_adv_passive(test_case: AdvTestCase) -> None:
  4406. """Test with passive data."""
  4407. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4408. adv_data = generate_advertisement_data(
  4409. manufacturer_data={2409: test_case.manufacturer_data},
  4410. rssi=-97,
  4411. )
  4412. result = parse_advertisement_data(ble_device, adv_data, test_case.modelName)
  4413. assert result == SwitchBotAdvertisement(
  4414. address="aa:bb:cc:dd:ee:ff",
  4415. data={
  4416. "rawAdvData": None,
  4417. "data": test_case.data,
  4418. "isEncrypted": False,
  4419. "model": test_case.model,
  4420. "modelFriendlyName": test_case.modelFriendlyName,
  4421. "modelName": test_case.modelName,
  4422. },
  4423. device=ble_device,
  4424. rssi=-97,
  4425. active=False,
  4426. )
  4427. @pytest.mark.parametrize(
  4428. "test_case",
  4429. [
  4430. AdvTestCase(
  4431. None,
  4432. b"=\x00\x00\x00",
  4433. {
  4434. 1: {
  4435. "isOn": True,
  4436. "power": 0.0,
  4437. "sequence_number": 138,
  4438. "switchMode": True,
  4439. },
  4440. 2: {
  4441. "isOn": True,
  4442. "power": 7.0,
  4443. "sequence_number": 138,
  4444. "switchMode": True,
  4445. },
  4446. },
  4447. "=",
  4448. "Relay Switch 2PM",
  4449. SwitchbotModel.RELAY_SWITCH_2PM,
  4450. ),
  4451. AdvTestCase(
  4452. None,
  4453. b";\x00\x00\x00",
  4454. {
  4455. "isOn": True,
  4456. "sequence_number": 71,
  4457. "switchMode": True,
  4458. },
  4459. ";",
  4460. "Relay Switch 1",
  4461. SwitchbotModel.RELAY_SWITCH_1,
  4462. ),
  4463. AdvTestCase(
  4464. None,
  4465. b"<\x00\x00\x00",
  4466. {
  4467. "isOn": True,
  4468. "power": 4.9,
  4469. "sequence_number": 71,
  4470. "switchMode": True,
  4471. },
  4472. "<",
  4473. "Relay Switch 1PM",
  4474. SwitchbotModel.RELAY_SWITCH_1PM,
  4475. ),
  4476. AdvTestCase(
  4477. None,
  4478. b">\x00\x00\x00",
  4479. {
  4480. "door_open": True,
  4481. "isOn": False,
  4482. "sequence_number": 15,
  4483. "switchMode": True,
  4484. },
  4485. ">",
  4486. "Garage Door Opener",
  4487. SwitchbotModel.GARAGE_DOOR_OPENER,
  4488. ),
  4489. AdvTestCase(
  4490. None,
  4491. b"\x00\x00\x00\x00\x10\xd0\xb1",
  4492. {},
  4493. b"\x00\x10\xd0\xb1",
  4494. "Strip Light 3",
  4495. SwitchbotModel.STRIP_LIGHT_3,
  4496. ),
  4497. AdvTestCase(
  4498. None,
  4499. b"\x00\x00\x00\x00\x10\xd0\xb0",
  4500. {},
  4501. b"\x00\x10\xd0\xb0",
  4502. "Floor Lamp",
  4503. SwitchbotModel.FLOOR_LAMP,
  4504. ),
  4505. AdvTestCase(
  4506. None,
  4507. b"\x00\x00\x00\x00\x11\x22\xb8",
  4508. {},
  4509. b"\x00\x11\x22\xb8",
  4510. "Candle Warmer Lamp",
  4511. SwitchbotModel.CANDLE_WARMER_LAMP,
  4512. ),
  4513. AdvTestCase(
  4514. None,
  4515. b"q\x00",
  4516. {},
  4517. "q",
  4518. "Ceiling Light",
  4519. SwitchbotModel.CEILING_LIGHT,
  4520. ),
  4521. AdvTestCase(
  4522. None,
  4523. b"r\x00d",
  4524. {},
  4525. "r",
  4526. "Light Strip",
  4527. SwitchbotModel.LIGHT_STRIP,
  4528. ),
  4529. AdvTestCase(
  4530. None,
  4531. b"u\x00d",
  4532. {},
  4533. "u",
  4534. "Color Bulb",
  4535. SwitchbotModel.COLOR_BULB,
  4536. ),
  4537. AdvTestCase(
  4538. None,
  4539. b"?\x00\x00\x00",
  4540. {
  4541. "isOn": True,
  4542. "power": 1254.0,
  4543. "sequence_number": 63,
  4544. "switchMode": True,
  4545. },
  4546. "?",
  4547. "Plug Mini (EU)",
  4548. SwitchbotModel.PLUG_MINI_EU,
  4549. ),
  4550. AdvTestCase(
  4551. None,
  4552. b"\x00\x00\x00\x00\x10\xd0\xb4",
  4553. {},
  4554. b"\x00\x10\xd0\xb4",
  4555. "RGBICWW Floor Lamp",
  4556. SwitchbotModel.RGBICWW_FLOOR_LAMP,
  4557. ),
  4558. AdvTestCase(
  4559. None,
  4560. b"\x00\x00\x00\x00\x11\xbb\x10",
  4561. {},
  4562. b"\x00\x11\xbb\x10",
  4563. "RGBICWW Ceiling Light",
  4564. SwitchbotModel.RGBICWW_CEILING_LIGHT,
  4565. ),
  4566. AdvTestCase(
  4567. None,
  4568. b"\x00\x00\x00\x00\x10\xd0\xb3",
  4569. {},
  4570. b"\x00\x10\xd0\xb3",
  4571. "RGBICWW Strip Light",
  4572. SwitchbotModel.RGBICWW_STRIP_LIGHT,
  4573. ),
  4574. AdvTestCase(
  4575. None,
  4576. b"\x00\x00\x00\x00\x10\xd0\xb7",
  4577. {},
  4578. b"\x00\x10\xd0\xb7",
  4579. "Permanent Outdoor Light",
  4580. SwitchbotModel.PERMANENT_OUTDOOR_LIGHT,
  4581. ),
  4582. AdvTestCase(
  4583. None,
  4584. b"\x00\x00M\x00\x10\xfb\xa8",
  4585. {},
  4586. b"\x00\x10\xfb\xa8",
  4587. "K11+ Vacuum",
  4588. SwitchbotModel.K11_VACUUM,
  4589. ),
  4590. AdvTestCase(
  4591. None,
  4592. b"\x00 _\x00\x10\xf3\xd8@",
  4593. {},
  4594. b"\x00\x10\xf3\xd8",
  4595. "Climate Panel",
  4596. SwitchbotModel.CLIMATE_PANEL,
  4597. ),
  4598. AdvTestCase(
  4599. None,
  4600. b"\x00 d\x00\x116@",
  4601. {},
  4602. b"\x00\x116@",
  4603. "Smart Thermostat Radiator",
  4604. SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
  4605. ),
  4606. AdvTestCase(
  4607. None,
  4608. b"\x00\x00d\x00\x10\xe0P",
  4609. {},
  4610. b"\x00\x10\xe0P",
  4611. "S20 Vacuum",
  4612. SwitchbotModel.S20_VACUUM,
  4613. ),
  4614. AdvTestCase(
  4615. None,
  4616. b"\x00 d\x00\x10\xcc\xc8",
  4617. {},
  4618. b"\x00\x10\xcc\xc8",
  4619. "Presence Sensor",
  4620. SwitchbotModel.PRESENCE_SENSOR,
  4621. ),
  4622. AdvTestCase(
  4623. None,
  4624. b"\x00\x007\x01\x11>\x10",
  4625. {},
  4626. b"\x01\x11>\x10",
  4627. "Art Frame",
  4628. SwitchbotModel.ART_FRAME,
  4629. ),
  4630. AdvTestCase(
  4631. None,
  4632. b"\x00\x00_\x01\x11\x03x",
  4633. {},
  4634. b"\x01\x11\x03x",
  4635. "Keypad Vision",
  4636. SwitchbotModel.KEYPAD_VISION,
  4637. ),
  4638. AdvTestCase(
  4639. None,
  4640. b"\x00\x00\x00\x00\x10\xd0\xb6",
  4641. {},
  4642. b"\x00\x10\xd0\xb6",
  4643. "RGBIC Neon Rope Light",
  4644. SwitchbotModel.RGBIC_NEON_ROPE_LIGHT,
  4645. ),
  4646. AdvTestCase(
  4647. None,
  4648. b"\x00\x00`\x01\x11Q\x98",
  4649. {},
  4650. b"\x01\x11Q\x98",
  4651. "Keypad Vision Pro",
  4652. SwitchbotModel.KEYPAD_VISION_PRO,
  4653. ),
  4654. ],
  4655. )
  4656. def test_adv_with_empty_data(test_case: AdvTestCase) -> None:
  4657. """Test with empty data."""
  4658. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4659. adv_data = generate_advertisement_data(
  4660. manufacturer_data={2409: None},
  4661. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": test_case.service_data},
  4662. rssi=-97,
  4663. )
  4664. result = parse_advertisement_data(ble_device, adv_data)
  4665. assert result == SwitchBotAdvertisement(
  4666. address="aa:bb:cc:dd:ee:ff",
  4667. data={
  4668. "rawAdvData": test_case.service_data,
  4669. "data": {},
  4670. "isEncrypted": False,
  4671. "model": test_case.model,
  4672. },
  4673. device=ble_device,
  4674. rssi=-97,
  4675. active=True,
  4676. )
  4677. def test_parse_advertisement_with_mac_cache() -> None:
  4678. """Test that populating the MAC cache helps identify unknown passive devices."""
  4679. # Clear the cache to ensure clean test
  4680. _MODEL_TO_MAC_CACHE.clear()
  4681. # Create a passive lock device with manufacturer data only (no service data)
  4682. # This would normally not be identifiable without the cache
  4683. mac_address = "C1:64:B8:7D:06:05"
  4684. ble_device = generate_ble_device(mac_address, "WoLock")
  4685. # Lock passive advertisement with manufacturer data
  4686. # This is real data from a WoLock device in passive mode
  4687. adv_data = generate_advertisement_data(
  4688. manufacturer_data={2409: b"\xc1d\xb8}\x06\x05\x00\x00\x00\x00\x00"},
  4689. service_data={},
  4690. rssi=-70,
  4691. )
  4692. # First attempt: Without cache, parser cannot identify the device model
  4693. result_without_cache = parse_advertisement_data(ble_device, adv_data)
  4694. assert result_without_cache is None, "Should not decode without model hint"
  4695. # Now populate the cache with the device's MAC and model
  4696. populate_model_to_mac_cache(mac_address, SwitchbotModel.LOCK)
  4697. # Second attempt: With cache, parser can now identify and decode the device
  4698. result_with_cache = parse_advertisement_data(ble_device, adv_data)
  4699. assert result_with_cache is not None, "Should decode with MAC cache"
  4700. assert result_with_cache.data["modelName"] == SwitchbotModel.LOCK
  4701. assert result_with_cache.data["modelFriendlyName"] == "Lock"
  4702. assert result_with_cache.active is False # Passive advertisement
  4703. # Clean up
  4704. _MODEL_TO_MAC_CACHE.clear()
  4705. def test_parse_advertisement_with_mac_cache_curtain() -> None:
  4706. """Test MAC cache with a passive curtain device."""
  4707. # Clear the cache
  4708. _MODEL_TO_MAC_CACHE.clear()
  4709. # Create a passive curtain device
  4710. mac_address = "CC:F4:C4:F9:AC:6C"
  4711. ble_device = generate_ble_device(mac_address, None)
  4712. # Curtain passive advertisement with only manufacturer data
  4713. adv_data = generate_advertisement_data(
  4714. manufacturer_data={2409: b"\xccOLG\x00c\x00\x00\x11\x00\x00"},
  4715. service_data={},
  4716. rssi=-85,
  4717. )
  4718. # Without cache, cannot identify
  4719. result_without_cache = parse_advertisement_data(ble_device, adv_data)
  4720. assert result_without_cache is None
  4721. # Populate cache
  4722. populate_model_to_mac_cache(mac_address, SwitchbotModel.CURTAIN)
  4723. # With cache, can identify and parse
  4724. result_with_cache = parse_advertisement_data(ble_device, adv_data)
  4725. assert result_with_cache is not None
  4726. assert result_with_cache.data["modelName"] == SwitchbotModel.CURTAIN
  4727. assert result_with_cache.data["modelFriendlyName"] == "Curtain 3"
  4728. assert result_with_cache.active is False
  4729. # Clean up
  4730. _MODEL_TO_MAC_CACHE.clear()
  4731. @pytest.mark.parametrize(
  4732. ("manufacturer_data", "service_data", "model"),
  4733. [
  4734. (b"\xff\xff\xff\xff", b"\xff\xff\xff\xff", None),
  4735. (b"\xff\xff\xff\xff", b"\xff\xff\xff\xff", "F"),
  4736. (b"\xff\xff\xff\xff\xff\xff\xff", b"\xff\xff\xff\xff\xff\xff\xff\xff", None),
  4737. (None, None, None),
  4738. ],
  4739. )
  4740. def test_with_invalid_advertisement(manufacturer_data, service_data, model) -> None:
  4741. """Test with invalid advertisement data."""
  4742. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4743. adv_data = generate_advertisement_data(
  4744. manufacturer_data={2409: manufacturer_data},
  4745. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": service_data},
  4746. rssi=-97,
  4747. )
  4748. result = parse_advertisement_data(ble_device, adv_data, model)
  4749. assert result is None
  4750. def test_weather_station_active() -> None:
  4751. """Test Weather Station active advertisement."""
  4752. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4753. adv_data = generate_advertisement_data(
  4754. manufacturer_data={
  4755. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x01\x50\x06\x9a\x23\x00\x00\x00\x00\x00"
  4756. },
  4757. service_data={
  4758. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x50\x00\x10\x53\xb0"
  4759. },
  4760. rssi=-67,
  4761. )
  4762. result = parse_advertisement_data(ble_device, adv_data)
  4763. assert result == SwitchBotAdvertisement(
  4764. address="aa:bb:cc:dd:ee:ff",
  4765. data={
  4766. "data": {
  4767. "battery": 80,
  4768. "fahrenheit": False,
  4769. "humidity": 35,
  4770. "temp": {"c": 26.6, "f": 79.88},
  4771. "temperature": 26.6,
  4772. },
  4773. "isEncrypted": False,
  4774. "model": b"\x00\x10\x53\xb0",
  4775. "modelFriendlyName": "Weather Station",
  4776. "modelName": SwitchbotModel.WEATHER_STATION,
  4777. "rawAdvData": b"\x00\x00\x50\x00\x10\x53\xb0",
  4778. },
  4779. device=ble_device,
  4780. rssi=-67,
  4781. active=True,
  4782. )
  4783. def test_weather_station_passive() -> None:
  4784. """Test Weather Station passive advertisement."""
  4785. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4786. adv_data = generate_advertisement_data(
  4787. manufacturer_data={
  4788. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x01\x50\x06\x9a\x23\x00\x00\x00\x00\x00"
  4789. },
  4790. rssi=-67,
  4791. )
  4792. result = parse_advertisement_data(
  4793. ble_device, adv_data, SwitchbotModel.WEATHER_STATION
  4794. )
  4795. assert result == SwitchBotAdvertisement(
  4796. address="aa:bb:cc:dd:ee:ff",
  4797. data={
  4798. "data": {
  4799. "battery": 80,
  4800. "fahrenheit": False,
  4801. "humidity": 35,
  4802. "temp": {"c": 26.6, "f": 79.88},
  4803. "temperature": 26.6,
  4804. },
  4805. "isEncrypted": False,
  4806. "model": b"\x00\x10\x53\xb0",
  4807. "modelFriendlyName": "Weather Station",
  4808. "modelName": SwitchbotModel.WEATHER_STATION,
  4809. "rawAdvData": None,
  4810. },
  4811. device=ble_device,
  4812. rssi=-67,
  4813. active=False,
  4814. )
  4815. def test_weather_station_service_data_only() -> None:
  4816. """Test Weather Station with service data only (no manufacturer data)."""
  4817. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4818. adv_data = generate_advertisement_data(
  4819. service_data={
  4820. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x50\x06\x9a\x23\x00\x10\x53\xb0"
  4821. },
  4822. rssi=-67,
  4823. )
  4824. result = parse_advertisement_data(
  4825. ble_device, adv_data, SwitchbotModel.WEATHER_STATION
  4826. )
  4827. assert result is not None
  4828. assert result.data["data"]["temperature"] == 26.6
  4829. assert result.data["data"]["humidity"] == 35
  4830. assert result.data["data"]["battery"] == 80
  4831. def test_weather_station_empty_data() -> None:
  4832. """Test Weather Station with empty/zero data returns empty dict."""
  4833. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4834. adv_data = generate_advertisement_data(
  4835. manufacturer_data={
  4836. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x01\x00\x00\x80\x00\x00\x00\x00\x00\x00"
  4837. },
  4838. service_data={
  4839. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x00\x00\x10\x53\xb0"
  4840. },
  4841. rssi=-67,
  4842. )
  4843. result = parse_advertisement_data(ble_device, adv_data)
  4844. assert result is not None
  4845. assert result.data["data"] == {}
  4846. def test_weather_station_positive_battery_and_empty_data() -> None:
  4847. """Test Weather Station with positive battery and empty/zero data returns empty dict."""
  4848. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4849. adv_data = generate_advertisement_data(
  4850. manufacturer_data={
  4851. 2409: b"\xaa\xbb\xcc\xdd\xee\xff\x01P\x00\x80\x00\x00\x00\x00\x00\x00"
  4852. },
  4853. service_data={
  4854. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00\x00\x00\x10\x53\xb0"
  4855. },
  4856. rssi=-67,
  4857. )
  4858. result = parse_advertisement_data(ble_device, adv_data)
  4859. assert result is not None
  4860. assert result.data["data"] == {}
  4861. def test_weather_station_short_service_data() -> None:
  4862. """Test Weather Station with too-short service data does not raise IndexError."""
  4863. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4864. adv_data = generate_advertisement_data(
  4865. service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x01"},
  4866. rssi=-67,
  4867. )
  4868. result = parse_advertisement_data(
  4869. ble_device, adv_data, SwitchbotModel.WEATHER_STATION
  4870. )
  4871. assert result is not None
  4872. assert result.data["data"] == {}
  4873. def test_weather_station_no_data() -> None:
  4874. """Test Weather Station with no usable data."""
  4875. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4876. adv_data = generate_advertisement_data(
  4877. manufacturer_data={2409: b"\xaa\xbb\xcc\xdd\xee"},
  4878. rssi=-67,
  4879. )
  4880. result = parse_advertisement_data(
  4881. ble_device, adv_data, SwitchbotModel.WEATHER_STATION
  4882. )
  4883. assert result is not None
  4884. assert result.data["data"] == {}
  4885. def test_with_special_manufacturer_data_length() -> None:
  4886. """Test with special manufacturer data length."""
  4887. ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
  4888. adv_data = generate_advertisement_data(
  4889. manufacturer_data={741: b"\xacg\xb2\xcd\xfa\xbe"},
  4890. service_data={
  4891. "0000fd3d-0000-1000-8000-00805f9b34fb": b"\xff\x80\x00\xf9\x80Bc\x00"
  4892. },
  4893. rssi=-97,
  4894. )
  4895. result = parse_advertisement_data(ble_device, adv_data)
  4896. assert result.data["modelName"] == SwitchbotModel.HUMIDIFIER