32_withings.pm 126 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672
  1. ##############################################################################
  2. # $Id: 32_withings.pm 17667 2018-11-03 21:53:19Z moises $
  3. #
  4. # 32_withings.pm
  5. #
  6. # 2018 Markus M.
  7. # Based on original code by justme1968
  8. #
  9. # https://forum.fhem.de/index.php/topic,64944.0.html
  10. #
  11. #
  12. ##############################################################################
  13. # Release 08 / 2018-09-28
  14. package main;
  15. use strict;
  16. use warnings;
  17. no warnings qw(redefine);
  18. use HttpUtils;
  19. use JSON;
  20. use POSIX qw( strftime );
  21. use Time::Local qw(timelocal);
  22. use Digest::SHA qw(hmac_sha1_base64);
  23. #use Encode qw(encode);
  24. #use LWP::Simple;
  25. #use HTTP::Request;
  26. #use HTTP::Cookies;
  27. #use URI::Escape qw(uri_escape);
  28. use Data::Dumper;
  29. #use Digest::MD5 qw(md5 md5_hex md5_base64);
  30. my %device_types = ( 0 => "User related",
  31. 1 => "Body Scale",
  32. 2 => "Camera",
  33. 4 => "Blood Pressure Monitor",
  34. 16 => "Activity Tracker",
  35. 32 => "Sleep Monitor",
  36. 64 => "Thermometer", );
  37. my %device_models = ( 1 => { 1 => "Smart Scale", 2 => "Wireless Scale", 3 => "Smart Kid Scale", 4 => "Smart Body Analyzer", 5 => "WiFi Body Scale", 6 => "Cardio Scale", 7 => "Body Scale", },
  38. 2 => { 21 => "Smart Baby Monitor", 22 => "Home", 23 => "Home v2", },
  39. 4 => { 41 => "iOS Blood Pressure Monitor", 42 => "Wireless Blood Pressure Monitor", 43 => "BPM", 44 => "BPM+", },
  40. 16 => { 51 => "Pulse Ox", 52 => "Activite", 53 => "Activite v2", 54 => "Go", 55 => "Steel HR", },
  41. 32 => { 60 => "Aura", 61 => "Sleep Sensor", 62 => "Sleep Mat", 63 => "Sleep", },
  42. 64 => { 70 => "Thermo", }, );
  43. #Firmware files: cdnfw_withings_net
  44. #Smart Body Analyzer: /wbs02/wbs02_1521.bin
  45. #Blood Pressure Monitor: /wpm02/wpm02_251.bin
  46. #Pulse: /wam01/wam01_1761.bin
  47. #Aura: /wsd01/wsd01_607.bin
  48. #Aura Mat: /wsm01/wsm01_711.bin
  49. #Home: /wbp02/wbp02_168.bin
  50. #Activite: /hwa01/hwa01_1070.bin
  51. my %measure_types = ( 1 => { name => "Weight (kg)", reading => "weight", },
  52. 4 => { name => "Height (meter)", reading => "height", },
  53. 5 => { name => "Lean Mass (kg)", reading => "fatFreeMass", },
  54. 6 => { name => "Fat Mass (%)", reading => "fatRatio", },
  55. 7 => { name => "Lean Mass (%)", reading => "fatFreeRatio", },
  56. 8 => { name => "Fat Mass (kg)", reading => "fatMassWeight", },
  57. 9 => { name => "Diastolic Blood Pressure (mmHg)", reading => "diastolicBloodPressure", },
  58. 10 => { name => "Systolic Blood Pressure (mmHg)", reading => "systolicBloodPressure", },
  59. 11 => { name => "Heart Rate (bpm)", reading => "heartPulse", },
  60. 12 => { name => "Temperature (°C)", reading => "temperature", },
  61. 13 => { name => "Humidity (%)", reading => "humidity", },
  62. 14 => { name => "unknown 14", reading => "unknown14", }, #device? event home - peak sound level?
  63. 15 => { name => "Noise (dB)", reading => "noise", },
  64. 18 => { name => "Weight Objective Speed", reading => "weightObjectiveSpeed", },
  65. 19 => { name => "Breastfeeding (s)", reading => "breastfeeding", }, #baby
  66. 20 => { name => "Bottle (ml)", reading => "bottle", }, #baby
  67. 22 => { name => "BMI", reading => "bmi", }, #user? goals
  68. 35 => { name => "CO2 (ppm)", reading => "co2", },
  69. 36 => { name => "Steps", reading => "steps", dailyreading => "dailySteps", }, #aggregate
  70. 37 => { name => "Elevation (m)", reading => "elevation", dailyreading => "dailyElevation", }, #aggregate
  71. 38 => { name => "Active Calories (kcal)", reading => "calories", dailyreading => "dailyCalories", }, #aggregate
  72. 39 => { name => "Intensity", reading => "intensity", }, #intraday only
  73. 40 => { name => "Distance (m)", reading => "distance", dailyreading => "dailyDistance", }, #aggregate #measure
  74. 41 => { name => "Descent (m)", reading => "descent", dailyreading => "dailyDescent", }, #descent #aggregate #measure ??sleepreading!
  75. 42 => { name => "Activity Type", reading => "activityType", }, #intraday only 1:walk 2:run
  76. 43 => { name => "Duration (s)", reading => "duration", }, #intraday only
  77. 44 => { name => "Sleep State", reading => "sleepstate", }, #intraday #aura mat
  78. 47 => { name => "MyFitnessPal Calories (kcal)", reading => "caloriesMFP", },
  79. 48 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, #day summary
  80. 49 => { name => "Idle Calories (kcal)", reading => "caloriesPassive", dailyreading => "dailyCaloriesPassive", }, #aggregate
  81. 50 => { name => "unknown 50", reading => "unknown50", dailyreading => "dailyUnknown50", }, #day summary pulse 60k-80k #aggregate
  82. 51 => { name => "Light Activity (s)", reading => "durationLight", dailyreading => "dailyDurationLight", }, #aggregate
  83. 52 => { name => "Moderate Activity (s)", reading => "durationModerate", dailyreading => "dailyDurationModerate", }, #aggregate
  84. 53 => { name => "Intense Activity (s)", reading => "durationIntense", dailyreading => "dailyDurationIntense", }, #aggregate
  85. 54 => { name => "SpO2 (%)", reading => "spo2", },
  86. 56 => { name => "Ambient light (lux)", reading => "light", }, # aura device
  87. 57 => { name => "Respiratory rate", reading => "breathing", }, # aura mat #measure vasistas
  88. 58 => { name => "Air Quality (ppm)", reading => "voc", }, # Home Air Quality
  89. 59 => { name => "unknown 59", reading => "unknown59", }, #
  90. 60 => { name => "unknown 60", reading => "unknown60", }, # aura mat #measure vasistas 20-200 peak 800
  91. 61 => { name => "unknown 61", reading => "unknown61", }, # aura mat #measure vasistas 10-60 peak 600
  92. 62 => { name => "unknown 62", reading => "unknown62", }, # aura mat #measure vasistas 20-100
  93. 63 => { name => "unknown 63", reading => "unknown63", }, # aura mat #measure vasistas 0-100
  94. 64 => { name => "unknown 64", reading => "unknown64", }, # aura mat #measure vasistas 800-1300
  95. 65 => { name => "unknown 65", reading => "unknown65", }, # aura mat #measure vasistas 3000-4500 peak 5000
  96. 66 => { name => "unknown 66", reading => "unknown66", }, # aura mat #measure vasistas 4000-7000
  97. 67 => { name => "unknown 67", reading => "unknown67", }, # aura mat #measure vasistas 0-500 peak 1500
  98. 68 => { name => "unknown 68", reading => "unknown68", }, # aura mat #measure vasistas 0-1500
  99. 69 => { name => "unknown 69", reading => "unknown69", }, # aura mat #measure vasistas 0-6000 peak 10000
  100. 70 => { name => "unknown 70", reading => "unknown70", }, #?
  101. 71 => { name => "Body Temperature (°C)", reading => "bodyTemperature", }, #thermo
  102. 73 => { name => "Skin Temperature (°C)", reading => "skinTemperature", }, #thermo
  103. 76 => { name => "Muscle Mass (kg)", reading => "muscleMass", }, # cardio scale
  104. 77 => { name => "Water Mass (kg)", reading => "waterMass", }, # cardio scale
  105. 78 => { name => "unknown 78", reading => "unknown78", }, # cardio scale
  106. 79 => { name => "unknown 79", reading => "unknown79", }, # body scale
  107. 80 => { name => "unknown 80", reading => "unknown80", }, # body scale
  108. 86 => { name => "unknown 86", reading => "unknown86", }, # body scale
  109. 87 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, # measures list sleepreading!
  110. 88 => { name => "Bone Mass (kg)", reading => "boneMassWeight", },
  111. 89 => { name => "unknown 89", reading => "unknown89", },
  112. 90 => { name => "unknown 90", reading => "unknown90", }, #pulse
  113. 91 => { name => "Pulse Wave Velocity (m/s)", reading => "pulseWave", }, # new weight
  114. 93 => { name => "Muscle Mass (%)", reading => "muscleRatio", }, # cardio scale
  115. 94 => { name => "Bone Mass (%)", reading => "boneRatio", }, # cardio scale
  116. 95 => { name => "Hydration (%)", reading => "hydration", }, # body water
  117. 122 => { name => "Pulse Transit Time (ms)", reading => "pulseTransitTime", },
  118. #-10 => { name => "Speed", reading => "speed", },
  119. #-11 => { name => "Pace", reading => "pace", },
  120. #-12 => { name => "Altitude", reading => "altitude", },
  121. );
  122. my %activity_types = ( 0 => "None",
  123. 1 => "Walking",
  124. 2 => "Running",
  125. 3 => "Hiking",
  126. 4 => "Skating",
  127. 5 => "BMX",
  128. 6 => "Cycling",
  129. 7 => "Swimming",
  130. 8 => "Surfing",
  131. 9 => "Kitesurfing",
  132. 10 => "Windsurfing",
  133. 11 => "Bodyboard",
  134. 12 => "Tennis",
  135. 13 => "Ping Pong",
  136. 14 => "Squash",
  137. 15 => "Badminton",
  138. 16 => "Weights",
  139. 17 => "Calisthenics",
  140. 18 => "Elliptical",
  141. 19 => "Pilates",
  142. 20 => "Basketball",
  143. 21 => "Soccer",
  144. 22 => "Football",
  145. 23 => "Rugby",
  146. 24 => "Vollyball",
  147. 25 => "Water Polo",
  148. 26 => "Horse Riding",
  149. 27 => "Golf",
  150. 28 => "Yoga",
  151. 29 => "Dancing",
  152. 30 => "Boxing",
  153. 31 => "Fencing",
  154. 32 => "Wrestling",
  155. 33 => "Martial Arts",
  156. 34 => "Skiing",
  157. 35 => "Snowboarding",
  158. 36 => "Other",
  159. 37 => "Sleep",
  160. 127 => "Sleep Debug",
  161. 128 => "No Activity",
  162. 187 => "Rowing",
  163. 188 => "Zumba",
  164. 190 => "Base",
  165. 191 => "Baseball",
  166. 192 => "Handball",
  167. 193 => "Hockey",
  168. 194 => "Ice Hockey",
  169. 195 => "Climbing",
  170. 196 => "Ice Skating",
  171. 271 => "Multi Sports",
  172. 272 => "Multi Sport", );
  173. my %sleep_state = ( 0 => "awake",
  174. 1 => "light sleep",
  175. 2 => "deep sleep",
  176. 3 => "REM sleep", );
  177. my %weight_units = ( 1 => { name => "kg (metric)", unit => "kg", },
  178. 2 => { name => "lb (US imperial)", unit => "lb", },
  179. 5 => { name => "stlb (UK imperial)", unit => "st", },
  180. 14 => { name => "stlb (UK imperial)", unit => "st", }, );
  181. my %distance_units = ( 6 => { name => "km", unit => "km", },
  182. 7 => { name => "miles", unit => "mi", }, );
  183. my %temperature_units = ( 11 => { name => "Celsius", unit => "˚C", },
  184. 13 => { name => "Fahrenheit", unit => "˚F", }, );
  185. my %height_units = ( 6 => { name => "cm", unit => "cm", },
  186. 7 => { name => "ft", unit => "ft", }, );
  187. my %aggregate_range = ( 1 => "day",
  188. 2 => "week",
  189. 3 => "month",
  190. 4 => "year",
  191. 5 => "alltime", );
  192. my %event_types = ( 10 => { name => "Noise", reading => "alertNoise", threshold => "levelNoise", duration => "durationNoise", unit => 0, },
  193. 11 => { name => "Motion", reading => "alertMotion", threshold => "levelMotion", duration => "durationMotion", unit => -2, },
  194. 12 => { name => "Low Temperature", reading => "alertTemperatureLow", threshold => "levelTemperatureLow", duration => "dummy", unit => -2, },
  195. 13 => { name => "High Temperature", reading => "alertTemperatureHigh", threshold => "levelTemperatureHigh", duration => "dummy", unit => -2, },
  196. 14 => { name => "Low Humidity", reading => "alertHumidityLow", threshold => "levelHumidityLow", duration => "dummy", unit => -2, },
  197. 15 => { name => "High Humidity", reading => "alertHumidityHigh", threshold => "levelHumidityHigh", duration => "dummy", unit => -2, },
  198. 20 => { name => "Disconnection", reading => "alertDisconnection", threshold => "levelDisconnected", duration => "dummy", unit => 0, },
  199. );
  200. #
  201. my %timeline_classes = ( 'noise_detected' => { name => "Noise", reading => "alertNoise", unit => 0, },
  202. 'movement_detected' => { name => "Motion", reading => "alertMotion", unit => 0, },
  203. 'alert_environment' => { name => "Air Quality Alert", reading => "alertEnvironment", unit => 0, },
  204. 'period_activity' => { name => "Activity Period", reading => "periodActivity", unit => 0, },
  205. 'period_activity_start' => { name => "Activity Period Start", reading => "periodActivityStart", unit => 0, },
  206. 'period_activity_cancel' => { name => "Activity Period Cancel", reading => "periodActivityCancel", unit => 0, },
  207. 'period_offline' => { name => "Offline Period", reading => "periodDisconnection", unit => 0, },
  208. 'offline' => { name => "Disconnection", reading => "alertDisconnection", unit => 0, },
  209. 'online' => { name => "Connection", reading => "alertConnection", unit => 0, },
  210. 'deleted' => { name => "Deleted", reading => "alertDeleted", unit => 0, },
  211. 'snapshot' => { name => "Snapshot", reading => "alertSnapshot", unit => 0, },
  212. );
  213. my %sleep_readings = ( 'lightsleepduration' => { name => "Light Sleep", reading => "sleepDurationLight", unit => "s", },
  214. 'deepsleepduration' => { name => "Deep Sleep", reading => "sleepDurationDeep", unit => "s", },
  215. 'remsleepduration' => { name => "REM Sleep", reading => "sleepDurationREM", unit => "s", },
  216. 'wakeupduration' => { name => "Awake In Bed", reading => "sleepDurationAwake", unit => "s", },
  217. 'wakeupcount' => { name => "Wakeup Count", reading => "wakeupCount", unit => 0, },
  218. 'durationtosleep' => { name => "Duration To Sleep", reading => "durationToSleep", unit => "s", },
  219. 'durationtowakeup' => { name => "Duration To Wake Up", reading => "durationToWakeUp", unit => "s", },
  220. 'sleepscore' => { name => "Sleep Score", reading => "sleepScore", unit => 0, },
  221. 'wsdid' => { name => "wsdid", reading => "wsdid", unit => 0, },
  222. 'hr_resting' => { name => "Resting HR", reading => "heartrateResting", unit => "bpm", },
  223. 'hr_min' => { name => "Minimum HR", reading => "heartrateMinimum", unit => "bpm", },
  224. 'hr_average' => { name => "Average HR", reading => "heartrateAverage", unit => "bpm", },
  225. 'hr_max' => { name => "Maximum HR", reading => "heartrateMaximum", unit => "bpm", },
  226. );
  227. my %alarm_sound = ( 0 => "Unknown",
  228. 1 => "Cloud Flakes",
  229. 2 => "Desert Wave",
  230. 3 => "Moss Forest",
  231. 4 => "Morning Smile",
  232. 5 => "Spotify",
  233. 6 => "Internet radio", );
  234. my %alarm_song = ( 'Unknown' => 0,
  235. 'Cloud Flakes' => 1,
  236. 'Desert Wave' => 2,
  237. 'Moss Forest' => 3,
  238. 'Morning Smile' => 4,
  239. 'Spotify' => 5,
  240. 'Internet radio' => 6, );
  241. my %nap_sound = ( 0 => "Unknown",
  242. 1 => "Celestial Piano (20 min)",
  243. 2 => "Cotton Cloud (10 min)",
  244. 3 => "Deep Smile (10 min)",
  245. 4 => "Sacred Forest (20 min)", );
  246. my %sleep_sound = ( 0 => "Unknown",
  247. 1 => "Moonlight Waves",
  248. 2 => "Siren's Whisper",
  249. 3 => "Celestial Piano",
  250. 4 => "Cloud Flakes",
  251. 5 => "Spotify",
  252. 6 => "Internet radio", );
  253. sub withings_Initialize($) {
  254. my ($hash) = @_;
  255. $hash->{DefFn} = "withings_Define";
  256. $hash->{SetFn} = "withings_Set";
  257. $hash->{GetFn} = "withings_Get";
  258. $hash->{NOTIFYDEV} = "global";
  259. $hash->{NotifyFn} = "withings_Notify";
  260. $hash->{UndefFn} = "withings_Undefine";
  261. $hash->{DbLog_splitFn} = "withings_DbLog_splitFn";
  262. $hash->{AttrFn} = "withings_Attr";
  263. $hash->{AttrList} = "IODev ".
  264. "disable:0,1 ".
  265. "intervalAlert ".
  266. "intervalData ".
  267. "intervalDebug ".
  268. "intervalProperties ".
  269. "intervalDaily ".
  270. "nossl:1 ".
  271. "IP ".
  272. "videoLinkEvents:1 ";
  273. $hash->{AttrList} .= $readingFnAttributes;
  274. Log3 "withings", 5, "withings: initialize";
  275. }
  276. #####################################
  277. sub withings_Define($$) {
  278. my ($hash, $def) = @_;
  279. Log3 "withings", 5, "withings: define ".$def;
  280. my @a = split("[ \t][ \t]*", $def);
  281. my $subtype;
  282. my $name = $a[0];
  283. if( @a == 3 ) {
  284. $subtype = "DEVICE";
  285. my $device = $a[2];
  286. $hash->{Device} = $device;
  287. my $d = $modules{$hash->{TYPE}}{defptr}{"D$device"};
  288. return "device $device already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
  289. $modules{$hash->{TYPE}}{defptr}{"D$device"} = $hash;
  290. } elsif( @a == 5 && $a[2] =~ m/^\D+$/ && $a[3] =~ m/^\d+$/ ) {
  291. $subtype = "DUMMY";
  292. my $device = $a[2];
  293. my $user = $a[3];
  294. $hash->{Device} = $device;
  295. $hash->{typeID} = '16';
  296. $hash->{modelID} = '0';
  297. $hash->{User} = $user;
  298. CommandAttr(undef,"$name IODev $a[4]");
  299. } elsif( @a == 4 && $a[2] =~ m/^\d+$/ && $a[3] =~ m/^[\w:-]+$/i ) {
  300. $subtype = "USER";
  301. my $user = $a[2];
  302. my $key = $a[3];
  303. my $accesskey = withings_encrypt($key);
  304. Log3 $name, 3, "$name: encrypt $key to $accesskey" if($key ne $accesskey);
  305. $hash->{DEF} = "$user $accesskey";
  306. $hash->{User} = $user;
  307. #$hash->{Key} = $accesskey; #not needed
  308. my $d = $modules{$hash->{TYPE}}{defptr}{"U$user"};
  309. return "device $user already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name );
  310. $modules{$hash->{TYPE}}{defptr}{"U$user"} = $hash;
  311. } elsif( @a == 4 || ($a[2] eq "ACCOUNT" && @a == 5 ) ) {
  312. $subtype = "ACCOUNT";
  313. my $user = $a[@a-2];
  314. my $pass = $a[@a-1];
  315. my $username = withings_encrypt($user);
  316. my $password = withings_encrypt($pass);
  317. Log3 $name, 3, "$name: encrypt $user/$pass to $username/$password" if($user ne $username || $pass ne $password);
  318. #$hash->{DEF} =~ s/$user/$username/g;
  319. #$hash->{DEF} =~ s/$pass/$password/g;
  320. $hash->{DEF} = "$username $password";
  321. $hash->{Clients} = ":withings:";
  322. $hash->{helper}{username} = $username;
  323. $hash->{helper}{password} = $password;
  324. $hash->{helper}{appliver} = '9855c478';
  325. $hash->{helper}{csrf_token} = '9855c478';
  326. } else {
  327. return "Usage: define <name> withings ACCOUNT <login> <password>" if(@a < 3 || @a > 5);
  328. }
  329. $hash->{NAME} = $name;
  330. $hash->{SUBTYPE} = $subtype if(defined($subtype));
  331. #CommandAttr(undef,"$name DbLogExclude .*");
  332. my $resolve = inet_aton("scalews.withings.com");
  333. if(!defined($resolve))
  334. {
  335. $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  336. InternalTimer( gettimeofday() + 900, "withings_InitWait", $hash, 0);
  337. return undef;
  338. }
  339. $hash->{STATE} = "Initialized" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  340. if( $init_done ) {
  341. withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" );
  342. withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" );
  343. withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" );
  344. InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0) if( $hash->{SUBTYPE} eq "DUMMY" );
  345. }
  346. else
  347. {
  348. InternalTimer(gettimeofday()+15, "withings_InitWait", $hash, 0);
  349. }
  350. return undef;
  351. }
  352. sub withings_InitWait($) {
  353. my ($hash) = @_;
  354. Log3 "withings", 5, "withings: initwait ".$init_done;
  355. RemoveInternalTimer($hash);
  356. my $resolve = inet_aton("scalews.withings.com");
  357. if(!defined($resolve))
  358. {
  359. $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  360. InternalTimer( gettimeofday() + 1800, "withings_InitWait", $hash, 0);
  361. return undef;
  362. }
  363. if( $init_done ) {
  364. withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" );
  365. withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" );
  366. withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" );
  367. InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0) if( $hash->{SUBTYPE} eq "DUMMY" );
  368. }
  369. else
  370. {
  371. InternalTimer(gettimeofday()+30, "withings_InitWait", $hash, 0);
  372. }
  373. return undef;
  374. }
  375. sub withings_Notify($$) {
  376. my ($hash,$dev) = @_;
  377. return if($dev->{NAME} ne "global");
  378. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  379. Log3 "withings", 5, "withings: notify";
  380. my $resolve = inet_aton("scalews.withings.com");
  381. if(!defined($resolve))
  382. {
  383. $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  384. InternalTimer( gettimeofday() + 3600, "withings_InitWait", $hash, 0);
  385. return undef;
  386. }
  387. withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" );
  388. withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" );
  389. withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" );
  390. }
  391. sub withings_Undefine($$) {
  392. my ($hash, $arg) = @_;
  393. Log3 "withings", 5, "withings: undefine";
  394. RemoveInternalTimer($hash);
  395. delete( $modules{$hash->{TYPE}}{defptr}{"U$hash->{User}"} ) if( $hash->{SUBTYPE} eq "USER" );
  396. delete( $modules{$hash->{TYPE}}{defptr}{"D$hash->{Device}"} ) if( $hash->{SUBTYPE} eq "DEVICE" );
  397. return undef;
  398. }
  399. sub withings_getToken($) {
  400. my ($hash) = @_;
  401. Log3 "withings", 5, "withings: gettoken";
  402. my $resolve = inet_aton("auth.withings.com");
  403. if(!defined($resolve))
  404. {
  405. Log3 "withings", 1, "withings: DNS error on getToken";
  406. return undef;
  407. }
  408. my ($err,$data) = HttpUtils_BlockingGet({
  409. url => $hash->{'.https'}."://auth.withings.com/index/service/once?action=get",
  410. timeout => 10,
  411. noshutdown => 1,
  412. data => {action => 'get'},
  413. });
  414. #my $URL = 'http://auth.withings.com/index/service/once?action=get';
  415. #my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30);
  416. #my $header = HTTP::Request->new(GET => $URL);
  417. #my $request = HTTP::Request->new('GET', $URL, $header);
  418. #my $response = $agent->request($request);
  419. return undef if(!defined($data));
  420. my $json = eval { JSON->new->utf8(0)->decode($data) };
  421. if($@)
  422. {
  423. Log3 "withings", 2, "withings: json evaluation error on getToken ".$@;
  424. return undef;
  425. }
  426. Log3 "withings", 1, "withings: getToken json error ".$json->{error} if(defined($json->{error}));
  427. my $once = $json->{body}{once};
  428. $hash->{Once} = $once;
  429. my $hashstring = withings_decrypt($hash->{helper}{username}).':'.md5_hex(withings_decrypt($hash->{helper}{password})).':'.$once;
  430. $hash->{Hash} = md5_hex($hashstring);
  431. }
  432. sub withings_getSessionKey($) {
  433. my ($hash) = @_;
  434. my $name = $hash->{NAME};
  435. return if( $hash->{SUBTYPE} ne "ACCOUNT" );
  436. return if( $hash->{SessionKey} && $hash->{SessionTimestamp} && gettimeofday() - $hash->{SessionTimestamp} < (60*60*24*7-3600) );
  437. my $resolve = inet_aton("account.withings.com");
  438. if(!defined($resolve))
  439. {
  440. $hash->{SessionTimestamp} = 0;
  441. Log3 $name, 1, "$name: DNS error on getSessionData";
  442. return undef;
  443. }
  444. $hash->{'.https'} = "https" if(!defined($hash->{'.https'}));
  445. # my $data1;
  446. # if( !defined($hash->{helper}{appliver}) || !defined($hash->{helper}{csrf_token}) || !defined($hash->{SessionTimestamp}) || gettimeofday() - $hash->{SessionTimestamp} > (30*60) )#!defined($hash->{helper}{appliver}) || !defined($hash->{helper}{csrf_token}))
  447. # {
  448. # my($err0,$data0) = HttpUtils_BlockingGet({
  449. # url => $hash->{'.https'}."://account.withings.com/",
  450. # timeout => 10,
  451. # noshutdown => 1,
  452. # });
  453. # if($err0 || !defined($data0))
  454. # {
  455. # Log3 $name, 1, "$name: appliver call failed! ".$err0;
  456. # return undef;
  457. # }
  458. # $data1 = $data0;
  459. # $data0 =~ /appliver=([^.*]+)\&/;
  460. # $hash->{helper}{appliver} = $1;
  461. # if(!defined($hash->{helper}{appliver})) {
  462. # Log3 $name, 1, "$name: APPLIVER ERROR ";
  463. # $hash->{STATE} = "APPLIVER error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  464. # return undef;
  465. # }
  466. # Log3 $name, 4, "$name: appliver ".$hash->{helper}{appliver};
  467. # #}
  468. #
  469. #
  470. # #if( !defined($hash->{helper}{csrf_token}) )
  471. # #{
  472. # $data1 =~ /csrf_token" value="(.*)"/;
  473. # $hash->{helper}{csrf_token} = $1;
  474. #
  475. # if(!defined($hash->{helper}{csrf_token})) {
  476. # Log3 $name, 1, "$name: CSRF ERROR ";
  477. # $hash->{STATE} = "CSRF error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  478. # return undef;
  479. # }
  480. # Log3 $name, 4, "$name: csrf_token ".$hash->{helper}{csrf_token};
  481. #}
  482. #my $ua = LWP::UserAgent->new;
  483. #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://account.withings.com/connectionuser/account_login?appname=my2&appliver='.$hash->{helper}{appliver}.'&r=https%3A%2F%2Fhealthmate.withings.com%2F',[email => withings_decrypt($hash->{helper}{username}), password => withings_decrypt($hash->{helper}{password}), is_admin => '',]);
  484. #my $get_data = 'use_authy=&is_admin=&email='.uri_escape(withings_decrypt($hash->{helper}{username})).'&password='.uri_escape(withings_decrypt($hash->{helper}{password}));
  485. #$request->content($get_data);
  486. #my $response = $ua->request($request);
  487. # $resolve = inet_aton("account.withings.com");
  488. # if(!defined($resolve))
  489. # {
  490. # Log3 $name, 1, "$name: DNS error on getSessionKey.";
  491. # return undef;
  492. # }
  493. my $datahash = {
  494. url => $hash->{'.https'}."://account.withings.com/connectionwou/account_login?r=https://healthmate.withings.com/",
  495. timeout => 10,
  496. noshutdown => 1,
  497. ignoreredirects => 1,
  498. data => { email=> withings_decrypt($hash->{helper}{username}), password => withings_decrypt($hash->{helper}{password}), is_admin => 'f' },
  499. };
  500. my($err,$data) = HttpUtils_BlockingGet($datahash);
  501. if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/)
  502. {
  503. Log3 $name, 1, "$name: LOGIN ERROR ";
  504. $hash->{STATE} = "Login error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  505. return undef;
  506. }
  507. else
  508. {
  509. if ($datahash->{httpheader} =~ /session_key=(.*?);/)
  510. {
  511. $hash->{SessionKey} = $1;
  512. $hash->{SessionTimestamp} = (gettimeofday())[0] if( $hash->{SessionKey} );
  513. $hash->{STATE} = "Connected" if( $hash->{SessionKey} );
  514. $hash->{STATE} = "Session error" if( !$hash->{SessionKey} );
  515. Log3 $name, 4, "$name: sessionkey ".$hash->{SessionKey};
  516. }
  517. else
  518. {
  519. $hash->{STATE} = "Cookie error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  520. Log3 $name, 1, "$name: COOKIE ERROR ";
  521. $hash->{helper}{appliver} = '9855c478';
  522. $hash->{helper}{csrf_token} = '9855c478';
  523. return undef;
  524. }
  525. }
  526. if( !$hash->{AccountID} || length($hash->{AccountID} < 2 ) ) {
  527. ($err,$data) = HttpUtils_BlockingGet({
  528. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/account",
  529. timeout => 10,
  530. noshutdown => 1,
  531. data => {sessionid => $hash->{SessionKey}, appname => 'my2', appliver=> $hash->{helper}{appliver}, apppfm => 'web', action => 'get', enrich => 't'},
  532. });
  533. return undef if(!defined($data));
  534. if( $data =~ m/^{.*}$/ )
  535. {
  536. my $json = eval { JSON->new->utf8(0)->decode($data) };
  537. if($@)
  538. {
  539. Log3 $name, 2, "$name: json evaluation error on getSessionKey ".$@;
  540. return undef;
  541. }
  542. Log3 $name, 1, "withings: getSessionKey json error ".$json->{error} if(defined($json->{error}));
  543. foreach my $account (@{$json->{body}{account}}) {
  544. next if( !defined($account->{id}) );
  545. if($account->{email} eq withings_decrypt($hash->{helper}{username}))
  546. {
  547. $hash->{AccountID} = $account->{id};
  548. }
  549. else
  550. {
  551. Log3 $name, 4, "$name: account email: ".$account->{email};
  552. }
  553. }
  554. Log3 $name, 4, "$name: accountid ".$hash->{AccountID};
  555. }
  556. else
  557. {
  558. $hash->{STATE} = "Account error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  559. Log3 $name, 1, "$name: ACCOUNT ERROR ";
  560. return undef;
  561. }
  562. }
  563. }
  564. sub withings_connect($) {
  565. my ($hash) = @_;
  566. my $name = $hash->{NAME};
  567. Log3 $name, 5, "$name: connect";
  568. $hash->{'.https'} = "https";
  569. $hash->{'.https'} = "http" if( AttrVal($name, "nossl", 0) );
  570. withings_getSessionKey( $hash );
  571. return undef; #no more autocreate on start
  572. foreach my $d (keys %defs) {
  573. next if(!defined($defs{$d}));
  574. next if($defs{$d}{TYPE} ne "autocreate");
  575. return undef if(IsDisabled($defs{$d}{NAME}));
  576. }
  577. my $autocreated = 0;
  578. my $users = withings_getUsers($hash);
  579. foreach my $user (@{$users}) {
  580. if( defined($modules{$hash->{TYPE}}{defptr}{"U$user->{id}"}) ) {
  581. Log3 $name, 2, "$name: user '$user->{id}' already defined";
  582. next;
  583. }
  584. next if($user->{usertype} ne "1" || $user->{status} ne "0");
  585. my $id = $user->{id};
  586. my $devname = "withings_U". $id;
  587. my $publickey = withings_encrypt($user->{publickey});
  588. my $define= "$devname withings $id $publickey";
  589. Log3 $name, 2, "$name: create new device '$devname' for user '$id'";
  590. my $cmdret= CommandDefine(undef,$define);
  591. if($cmdret) {
  592. Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
  593. } else {
  594. $cmdret= CommandAttr(undef,"$devname alias ".$user->{shortname});
  595. #$cmdret= CommandAttr(undef,"$devname room WithingsTest");
  596. $cmdret= CommandAttr(undef,"$devname IODev $name");
  597. #$cmdret= CommandAttr(undef,"$devname disable 1");
  598. #$cmdret= CommandAttr(undef,"$devname verbose 5");
  599. $autocreated++;
  600. }
  601. }
  602. my $devices = withings_getDevices($hash);
  603. foreach my $device (@{$devices}) {
  604. if( defined($modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"}) ) {
  605. my $d = $modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"};
  606. $d->{association} = $device->{association} if($device->{association});
  607. #get user from association
  608. if(defined($device->{deviceproperties})){
  609. $d->{User} = $device->{deviceproperties}{linkuserid} if(defined($device->{deviceproperties}{linkuserid}));
  610. $d->{color} = $device->{deviceproperties}{product_color} if(defined($device->{deviceproperties}{product_color}));
  611. }
  612. Log3 $name, 2, "$name: device '$device->{deviceid}' already defined";
  613. next;
  614. }
  615. my $detail = $device->{deviceproperties};
  616. next if( !defined($detail->{id}) );
  617. my $id = $detail->{id};
  618. my $devname = "withings_D". $id;
  619. my $define= "$devname withings $id";
  620. Log3 $name, 2, "$name: create new device '$devname' for device '$id'";
  621. my $cmdret= CommandDefine(undef,$define);
  622. if($cmdret) {
  623. Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
  624. } else {
  625. $cmdret= CommandAttr(undef,"$devname alias ".$device_types{$detail->{type}}) if( defined($device_types{$detail->{type}}) );
  626. $cmdret= CommandAttr(undef,"$devname alias ".$device_models{$detail->{type}}->{$detail->{model}}) if( defined($device_models{$detail->{type}}) && defined($device_models{$detail->{type}}->{$detail->{model}}) );
  627. #$cmdret= CommandAttr(undef,"$devname room WithingsTest");
  628. $cmdret= CommandAttr(undef,"$devname IODev $name");
  629. #$cmdret= CommandAttr(undef,"$devname disable 1");
  630. #$cmdret= CommandAttr(undef,"$devname verbose 5");
  631. $autocreated++;
  632. }
  633. }
  634. CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) );
  635. }
  636. sub withings_autocreate($) {
  637. my ($hash) = @_;
  638. my $name = $hash->{NAME};
  639. Log3 $name, 5, "$name: autocreate";
  640. $hash->{'.https'} = "https";
  641. $hash->{'.https'} = "http" if( AttrVal($name, "nossl", 0) );
  642. withings_getSessionKey( $hash );
  643. my $autocreated = 0;
  644. my $users = withings_getUsers($hash);
  645. foreach my $user (@{$users}) {
  646. if( defined($modules{$hash->{TYPE}}{defptr}{"U$user->{id}"}) ) {
  647. Log3 $name, 2, "$name: user '$user->{id}' already defined";
  648. next;
  649. }
  650. next if($user->{usertype} ne "1" || $user->{status} ne "0");
  651. my $id = $user->{id};
  652. my $devname = "withings_U". $id;
  653. my $publickey = withings_encrypt($user->{publickey});
  654. my $define= "$devname withings $id $publickey";
  655. Log3 $name, 2, "$name: create new device '$devname' for user '$id'";
  656. my $cmdret= CommandDefine(undef,$define);
  657. if($cmdret) {
  658. Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
  659. } else {
  660. $cmdret= CommandAttr(undef,"$devname alias ".$user->{shortname});
  661. $cmdret= CommandAttr(undef,"$devname IODev $name");
  662. $cmdret= CommandAttr(undef,"$devname room Withings");
  663. $autocreated++;
  664. }
  665. }
  666. my $devices = withings_getDevices($hash);
  667. foreach my $device (@{$devices}) {
  668. if( defined($modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"}) ) {
  669. my $d = $modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"};
  670. $d->{association} = $device->{association} if($device->{association});
  671. #get user from association
  672. if(defined($device->{deviceproperties})){
  673. $d->{User} = $device->{deviceproperties}{linkuserid} if(defined($device->{deviceproperties}{linkuserid}));
  674. $d->{color} = $device->{deviceproperties}{product_color} if(defined($device->{deviceproperties}{product_color}));
  675. }
  676. Log3 $name, 2, "$name: device '$device->{deviceid}' already defined";
  677. next;
  678. }
  679. my $detail = $device->{deviceproperties};
  680. next if( !defined($detail->{id}) );
  681. my $id = $detail->{id};
  682. my $devname = "withings_D". $id;
  683. my $define= "$devname withings $id";
  684. Log3 $name, 2, "$name: create new device '$devname' for device '$id'";
  685. my $cmdret= CommandDefine(undef,$define);
  686. if($cmdret) {
  687. Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
  688. } else {
  689. $cmdret= CommandAttr(undef,"$devname alias ".$device_types{$detail->{type}}) if( defined($device_types{$detail->{type}}) );
  690. $cmdret= CommandAttr(undef,"$devname alias ".$device_models{$detail->{type}}->{$detail->{model}}) if( defined($device_models{$detail->{type}}) && defined($device_models{$detail->{type}}->{$detail->{model}}) );
  691. $cmdret= CommandAttr(undef,"$devname IODev $name");
  692. $cmdret= CommandAttr(undef,"$devname room Withings");
  693. $autocreated++;
  694. }
  695. }
  696. CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) );
  697. }
  698. sub withings_initDevice($) {
  699. my ($hash) = @_;
  700. my $name = $hash->{NAME};
  701. Log3 $name, 5, "$name: initdevice ".$hash->{Device};
  702. AssignIoPort($hash);
  703. if(defined($hash->{IODev}->{NAME})) {
  704. Log3 $name, 2, "$name: I/O device is " . $hash->{IODev}->{NAME};
  705. } else {
  706. Log3 $name, 1, "$name: no I/O device";
  707. }
  708. $hash->{'.https'} = "https";
  709. $hash->{'.https'} = "http" if( AttrVal($hash->{NAME}, "nossl", 0) );
  710. my $device = withings_getDeviceDetail( $hash );
  711. $hash->{DeviceType} = "UNKNOWN";
  712. $hash->{sn} = $device->{sn};
  713. $hash->{fw} = $device->{fw};
  714. $hash->{created} = $device->{created};
  715. $hash->{location} = $device->{latitude}.",".$device->{longitude} if(defined($device->{latitude}));
  716. $hash->{DeviceType} = $device->{type};
  717. $hash->{DeviceType} = $device_types{$device->{type}} if( defined($device->{type}) && defined($device_types{$device->{type}}) );
  718. $hash->{model} = $device->{model};
  719. $hash->{model} = $device_models{$device->{type}}->{$device->{model}}
  720. if( defined($device->{type}) && defined($device->{model}) && defined($device_models{$device->{type}}) && defined($device_models{$device->{type}}->{$device->{model}}) );
  721. $hash->{modelID} = $device->{model};
  722. $hash->{typeID} = $device->{type};
  723. $hash->{lastsessiondate} = $device->{lastsessiondate} if( defined($device->{lastsessiondate}) );
  724. $hash->{lastweighindate} = $device->{lastweighindate} if( defined($device->{lastweighindate}) );
  725. if((defined($hash->{typeID}) && $hash->{typeID} == 16) or (defined($hash->{typeID}) && $hash->{typeID} == 32 && defined($hash->{modelID}) && $hash->{modelID} != 60))
  726. {
  727. my $devicelink = withings_getDeviceLink( $hash );
  728. if(defined($devicelink) && defined($devicelink->{linkuserid}))
  729. {
  730. $hash->{User} = $devicelink->{linkuserid};
  731. $hash->{UserDevice} = $modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{linkuserid}} if defined($modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{linkuserid}});
  732. }
  733. }
  734. if( !defined( $attr{$name}{stateFormat} ) ) {
  735. $attr{$name}{stateFormat} = "batteryPercent %";
  736. $attr{$name}{stateFormat} = "co2 ppm" if( $device->{model} == 4 );
  737. $attr{$name}{stateFormat} = "voc ppm" if( $device->{model} == 22 );
  738. $attr{$name}{stateFormat} = "light lux" if( $device->{model} == 60 );
  739. $attr{$name}{stateFormat} = "lastWeighinDate" if( $device->{model} == 61 );
  740. }
  741. withings_readAuraAlarm($hash) if( defined(AttrVal($name,"IP",undef)) && defined($device->{model}) && $device->{model} == 60 && defined($device->{type}) && $device->{type} == 32 );
  742. InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0);
  743. }
  744. sub withings_initUser($) {
  745. my ($hash) = @_;
  746. my $name = $hash->{NAME};
  747. Log3 $name, 5, "$name: inituser ".$hash->{User};
  748. AssignIoPort($hash);
  749. if(defined($hash->{IODev}->{NAME})) {
  750. Log3 $name, 2, "$name: I/O device is " . $hash->{IODev}->{NAME};
  751. } else {
  752. Log3 $name, 1, "$name: no I/O device";
  753. }
  754. $hash->{'.https'} = "https";
  755. $hash->{'.https'} = "http" if( AttrVal($hash->{NAME}, "nossl", 0) );
  756. my $user = withings_getUserDetail( $hash );
  757. $hash->{shortName} = $user->{shortname};
  758. $hash->{gender} = ($user->{gender}==0)?"male":"female" if( defined($user->{gender}) );
  759. $hash->{userName} = ($user->{firstname}?$user->{firstname}:"") ." ". ($user->{lastname}?$user->{lastname}:"");
  760. $hash->{birthdate} = strftime("%Y-%m-%d", localtime($user->{birthdate})) if( defined($user->{birthdate}) );
  761. $hash->{age} = sprintf("%.1f",((int(time()) - int($user->{birthdate}))/(60*60*24*365.24225))) if( defined($user->{birthdate}) );
  762. $hash->{created} = $user->{created};
  763. $hash->{modified} = $user->{modified};
  764. $attr{$name}{stateFormat} = "weight kg" if( !defined( $attr{$name}{stateFormat} ) );
  765. InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0);
  766. }
  767. sub withings_getUsers($) {
  768. my ($hash) = @_;
  769. my $name = $hash->{NAME};
  770. Log3 $name, 5, "$name: getusers";
  771. withings_getSessionKey($hash);
  772. my ($err,$data) = HttpUtils_BlockingGet({
  773. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/account",
  774. timeout => 10,
  775. noshutdown => 1,
  776. data => {sessionid => $hash->{SessionKey}, accountid => $hash->{AccountID} , recurse_use => '1', recurse_devtype => '1', listmask => '5', allusers => 't' , appname => 'my2', appliver=> $hash->{helper}{appliver}, apppfm => 'web', action => 'getuserslist'},
  777. });
  778. #my $ua = LWP::UserAgent->new;
  779. #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://healthmate.withings.com/index/service/account');
  780. #my $get_data = 'sessionid='.$hash->{SessionKey}.'&accountid='.$hash->{AccountID}.'&recurse_use=1&recurse_devtype=1&listmask=5&allusers=t&appname=my2&appliver='.$hash->{helper}{appliver}.'&apppfm=web&action=getuserslist';
  781. #$request->content($get_data);
  782. #my $response = $ua->request($request);
  783. return undef if(!defined($data));
  784. my $json = eval { JSON->new->utf8(0)->decode($data) };
  785. if($@)
  786. {
  787. Log3 $name, 2, "$name: json evaluation error on getUsers ".$@;
  788. return undef;
  789. }
  790. Log3 $name, 1, "withings: getUsers json error ".$json->{error} if(defined($json->{error}));
  791. my @users = ();
  792. foreach my $user (@{$json->{body}{users}}) {
  793. next if( !defined($user->{id}) );
  794. push( @users, $user );
  795. }
  796. return \@users;
  797. }
  798. sub withings_getDevices($) {
  799. my ($hash) = @_;
  800. my $name = $hash->{NAME};
  801. Log3 $name, 5, "$name: getdevices";
  802. withings_getSessionKey($hash);
  803. my ($err,$data) = HttpUtils_BlockingGet({
  804. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/association",
  805. timeout => 10,
  806. noshutdown => 1,
  807. data => {sessionid => $hash->{SessionKey}, accountid => $hash->{AccountID} , type => '-1', enrich => 't' , appname => 'my2', appliver=> $hash->{helper}{appliver}, apppfm => 'web', action => 'getbyaccountid'},
  808. });
  809. #my $ua = LWP::UserAgent->new;
  810. #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://scalews.withings.com/cgi-bin/association');
  811. #my $get_data = 'sessionid='.$hash->{SessionKey}.'&accountid='.$hash->{AccountID}.'&type=-1&enrich=t&appname=my2&appliver='.$hash->{helper}{appliver}.'&apppfm=web&action=getbyaccountid';
  812. #$request->content($get_data);
  813. #my $response = $ua->request($request);
  814. return undef if(!defined($data));
  815. my $json = eval { JSON->new->utf8(0)->decode($data) };
  816. if($@)
  817. {
  818. Log3 $name, 2, "$name: json evaluation error on getDevices ".$@;
  819. return undef;
  820. }
  821. Log3 $name, 1, "withings: getDevices json error ".$json->{error} if(defined($json->{error}));
  822. Log3 $name, 5, "$name: getdevices ".Dumper($json);
  823. my @devices = ();
  824. foreach my $association (@{$json->{body}{associations}}) {
  825. next if( !defined($association->{deviceid}) );
  826. push( @devices, $association );
  827. }
  828. return \@devices;
  829. }
  830. sub withings_getDeviceDetail($) {
  831. my ($hash) = @_;
  832. my $name = $hash->{NAME};
  833. Log3 $name, 5, "$name: getdevicedetail ".$hash->{Device};
  834. return undef if( !defined($hash->{IODev}) );
  835. withings_getSessionKey( $hash->{IODev} );
  836. my ($err,$data) = HttpUtils_BlockingGet({
  837. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/device",
  838. timeout => 10,
  839. noshutdown => 1,
  840. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid => $hash->{Device} , appname => 'my2', appliver=> $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getproperties'},
  841. });
  842. #Log3 $name, 5, "$name: getdevicedetaildata ".Dumper($data);
  843. return undef if(!defined($data));
  844. my $json = eval { JSON->new->utf8(0)->decode($data) };
  845. if($@)
  846. {
  847. Log3 $name, 2, "$name: json evaluation error on getDeviceDetail ".$@;
  848. return undef;
  849. }
  850. Log3 $name, 1, "withings: getDeviceDetail json error ".$json->{error} if(defined($json->{error}));
  851. if($json)
  852. {
  853. my $device = $json->{body};
  854. $hash->{sn} = $device->{sn};
  855. $hash->{fw} = $device->{fw};
  856. $hash->{created} = $device->{created};
  857. $hash->{location} = $device->{latitude}.",".$device->{longitude} if(defined($device->{latitude}));
  858. $hash->{DeviceType} = $device->{type};
  859. $hash->{DeviceType} = $device_types{$device->{type}} if( defined($device->{type}) && defined($device_types{$device->{type}}) );
  860. $hash->{model} = $device->{model};
  861. $hash->{model} = $device_models{$device->{type}}->{$device->{model}}
  862. if( defined($device->{type}) && defined($device->{model}) && defined($device_models{$device->{type}}) && defined($device_models{$device->{type}}->{$device->{model}}) );
  863. $hash->{modelID} = $device->{model};
  864. $hash->{typeID} = $device->{type};
  865. $hash->{lastsessiondate} = $device->{lastsessiondate} if( defined($device->{lastsessiondate}) );
  866. $hash->{lastweighindate} = $device->{lastweighindate} if( defined($device->{lastweighindate}) );
  867. }
  868. return $json->{body};
  869. }
  870. sub withings_getDeviceLink($) {
  871. my ($hash) = @_;
  872. my $name = $hash->{NAME};
  873. Log3 $name, 5, "$name: getdevicelink ".$hash->{Device};
  874. return undef if( !defined($hash->{IODev}) );
  875. withings_getSessionKey( $hash->{IODev} );
  876. my ($err,$data) = HttpUtils_BlockingGet({
  877. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/association",
  878. timeout => 10,
  879. noshutdown => 1,
  880. data => {sessionid => $hash->{IODev}->{SessionKey}, appname => 'hmw', appliver=> $hash->{IODev}->{helper}{appliver}, enrich => 't', action => 'getbyaccountid'},
  881. });
  882. #my $ua = LWP::UserAgent->new;
  883. #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://healthmate.withings.com/index/service/v2/link');
  884. #my $get_data = 'sessionid='.$hash->{IODev}->{SessionKey}.'&deviceid='.$hash->{Device}.'&appname=my2&appliver='.$hash->{IODev}->{helper}{appliver}.'&apppfm=web&action=get';
  885. #$request->content($get_data);
  886. #my $response = $ua->request($request);
  887. return undef if(!defined($data));
  888. my $json = eval { JSON->new->utf8(0)->decode($data) };
  889. if($@)
  890. {
  891. Log3 $name, 2, "$name: json evaluation error on getDeviceLink ".$@;
  892. return undef;
  893. }
  894. Log3 $name, 1, "withings: getDeviceLink json error ".$json->{error} if(defined($json->{error}));
  895. foreach my $association (@{$json->{body}{associations}}) {
  896. next if( !defined($association->{deviceid}) );
  897. next if( $association->{deviceid} ne $hash->{Device} );
  898. return $association->{deviceproperties};
  899. }
  900. return undef;
  901. }
  902. sub withings_getDeviceProperties($) {
  903. my ($hash) = @_;
  904. my $name = $hash->{NAME};
  905. Log3 $name, 5, "$name: getdeviceproperties ".$hash->{Device};
  906. return undef if( !defined($hash->{Device}) );
  907. return undef if( !defined($hash->{IODev}) );
  908. withings_getSessionKey( $hash->{IODev} );
  909. HttpUtils_NonblockingGet({
  910. url => "https://scalews.withings.com/cgi-bin/device",
  911. timeout => 30,
  912. noshutdown => 1,
  913. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getproperties'},
  914. hash => $hash,
  915. type => 'deviceProperties',
  916. callback => \&withings_Dispatch,
  917. });
  918. my ($seconds) = gettimeofday();
  919. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  920. readingsSingleUpdate( $hash, ".pollProperties", $seconds, 0 );
  921. return undef;
  922. }
  923. sub withings_getDeviceReadingsScale($) {
  924. my ($hash) = @_;
  925. my $name = $hash->{NAME};
  926. Log3 $name, 5, "$name: getscalereadings ".$hash->{Device};
  927. return undef if( !defined($hash->{Device}) );
  928. return undef if( !defined($hash->{IODev}) );
  929. withings_getSessionKey( $hash->{IODev} );
  930. my ($now) = time;
  931. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  932. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  933. my $enddate = ($lastupdate+(24*60*60));
  934. $enddate = $now if ($enddate > $now);
  935. HttpUtils_NonblockingGet({
  936. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  937. timeout => 30,
  938. noshutdown => 1,
  939. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, meastype => '12,35', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getmeashf'},
  940. hash => $hash,
  941. type => 'deviceReadingsScale',
  942. enddate => int($enddate),
  943. callback => \&withings_Dispatch,
  944. });
  945. my ($seconds) = gettimeofday();
  946. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  947. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  948. return undef;
  949. }
  950. sub withings_getDeviceReadingsBedside($) {
  951. my ($hash) = @_;
  952. my $name = $hash->{NAME};
  953. Log3 $name, 5, "$name: getaurareadings ".$hash->{Device};
  954. return undef if( !defined($hash->{Device}) );
  955. return undef if( !defined($hash->{IODev}) );
  956. withings_getSessionKey( $hash->{IODev} );
  957. my ($now) = time;
  958. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  959. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  960. my $enddate = ($lastupdate+(8*60*60));
  961. $enddate = $now if ($enddate > $now);
  962. HttpUtils_NonblockingGet({
  963. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  964. timeout => 30,
  965. noshutdown => 1,
  966. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, meastype => '12,13,14,15,56', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getmeashf'},
  967. hash => $hash,
  968. type => 'deviceReadingsBedside',
  969. enddate => int($enddate),
  970. callback => \&withings_Dispatch,
  971. });
  972. my ($seconds) = gettimeofday();
  973. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  974. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  975. return undef;
  976. }
  977. sub withings_getDeviceReadingsHome($) {
  978. my ($hash) = @_;
  979. my $name = $hash->{NAME};
  980. Log3 $name, 5, "$name: gethomereadings ".$hash->{Device};
  981. return undef if( !defined($hash->{Device}) );
  982. return undef if( !defined($hash->{IODev}) );
  983. withings_getSessionKey( $hash->{IODev} );
  984. my ($now) = time;
  985. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  986. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  987. my $enddate = ($lastupdate+(8*60*60));
  988. $enddate = $now if ($enddate > $now);
  989. HttpUtils_NonblockingGet({
  990. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  991. timeout => 30,
  992. noshutdown => 1,
  993. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, meastype => '12,13,14,15,58', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getmeashf'},
  994. hash => $hash,
  995. type => 'deviceReadingsHome',
  996. enddate => int($enddate),
  997. callback => \&withings_Dispatch,
  998. });
  999. my ($seconds) = gettimeofday();
  1000. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1001. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  1002. return undef;
  1003. }
  1004. sub withings_getDeviceEventsBaby($) {
  1005. my ($hash) = @_;
  1006. my $name = $hash->{NAME};
  1007. Log3 $name, 5, "$name: getbabyevents ".$hash->{Device};
  1008. return undef if( !defined($hash->{Device}) );
  1009. return undef if( !defined($hash->{IODev}) );
  1010. withings_getSessionKey( $hash->{IODev} );
  1011. my ($now) = time;
  1012. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  1013. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1014. HttpUtils_NonblockingGet({
  1015. url => "https://scalews.withings.com/index/service/event",
  1016. timeout => 30,
  1017. noshutdown => 1,
  1018. data => {activated => '0', action => 'get', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, type => '10,11,12,13,14,15,20', begindate => int($lastupdate)},
  1019. hash => $hash,
  1020. type => 'deviceReadingsBaby',
  1021. callback => \&withings_Dispatch,
  1022. });
  1023. my ($seconds) = gettimeofday();
  1024. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1025. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  1026. return undef;
  1027. }
  1028. sub withings_getDeviceAlertsHome($) {
  1029. my ($hash) = @_;
  1030. my $name = $hash->{NAME};
  1031. Log3 $name, 5, "$name: gethomealerts ".$hash->{Device};
  1032. return undef if( !defined($hash->{Device}) );
  1033. return undef if( !defined($hash->{IODev}) );
  1034. withings_getSessionKey( $hash->{IODev} );
  1035. my ($now) = time;
  1036. my $lastupdate = ReadingsVal( $name, ".lastAlert", ($now-7*24*60*60) );#$hash->{created} );#
  1037. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1038. HttpUtils_NonblockingGet({
  1039. url => "https://scalews.withings.com/cgi-bin/v2/timeline",
  1040. timeout => 30,
  1041. noshutdown => 1,
  1042. data => {type => '1', callctx => 'foreground', action => 'getbydeviceid', appname => 'HomeMonitor', apppfm => 'ios', appliver => '20000', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, lastupdate => int($lastupdate) },
  1043. hash => $hash,
  1044. type => 'deviceAlertsHome',
  1045. callback => \&withings_Dispatch,
  1046. });
  1047. my ($seconds) = gettimeofday();
  1048. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1049. readingsSingleUpdate( $hash, ".pollAlert", $seconds, 0 );
  1050. return undef;
  1051. }
  1052. sub withings_getDeviceAlertsBaby($) {
  1053. my ($hash) = @_;
  1054. my $name = $hash->{NAME};
  1055. Log3 $name, 5, "$name: getbabyevents ".$hash->{Device};
  1056. return undef if( !defined($hash->{Device}) );
  1057. return undef if( !defined($hash->{IODev}) );
  1058. withings_getSessionKey( $hash->{IODev} );
  1059. my ($now) = time;
  1060. my $lastupdate = ReadingsVal( $name, ".lastAlert", ($now-120*60) );
  1061. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1062. HttpUtils_NonblockingGet({
  1063. url => "https://scalews.withings.com/index/service/event",
  1064. timeout => 30,
  1065. noshutdown => 1,
  1066. data => {activated => '1', action => 'get', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, type => '10,11,12,13,14,15,20', begindate => int($lastupdate)},
  1067. hash => $hash,
  1068. type => 'deviceAlertsBaby',
  1069. callback => \&withings_Dispatch,
  1070. });
  1071. my ($seconds) = gettimeofday();
  1072. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1073. readingsSingleUpdate( $hash, ".pollAlert", $seconds, 0 );
  1074. return undef;
  1075. }
  1076. sub withings_getVideoLink($) {
  1077. my ($hash) = @_;
  1078. my $name = $hash->{NAME};
  1079. Log3 $name, 5, "$name: getbabyvideo ".$hash->{Device};
  1080. return undef if( !defined($hash->{Device}) );
  1081. return undef if( !defined($hash->{IODev}) );
  1082. withings_getSessionKey( $hash->{IODev} );
  1083. my ($err,$data) = HttpUtils_BlockingGet({
  1084. url => $hash->{'.https'}."://babyws.withings.net/cgi-bin/presence",
  1085. timeout => 10,
  1086. noshutdown => 1,
  1087. data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid => $hash->{Device} , action => 'get'},
  1088. });
  1089. return undef if(!defined($data));
  1090. my $json = eval { JSON->new->utf8(0)->decode($data) };
  1091. if($@)
  1092. {
  1093. Log3 $name, 2, "$name: json evaluation error on getVideoLink ".$@;
  1094. return undef;
  1095. }
  1096. Log3 $name, 1, "withings: getVideoLink json error ".$json->{error} if(defined($json->{error}));
  1097. if(defined($json->{body}{device}))
  1098. {
  1099. $hash->{videolink_ext} = "http://fpdownload.adobe.com/strobe/FlashMediaPlayback_101.swf?streamType=live&autoPlay=true&playButtonOverlay=false&src=rtmp://".$json->{body}{device}{proxy_ip}.":".$json->{body}{device}{proxy_port}."/".$json->{body}{device}{kp_hash}."/";
  1100. $hash->{videolink_int} = "http://fpdownload.adobe.com/strobe/FlashMediaPlayback_101.swf?streamType=live&autoPlay=true&playButtonOverlay=false&src=rtmp://".$json->{body}{device}{private_ip}.":".$json->{body}{device}{proxy_port}."/".$json->{body}{device}{kd_hash}."/";
  1101. }
  1102. return $json;
  1103. }
  1104. sub withings_getS3Credentials($) {
  1105. my ($hash) = @_;
  1106. my $name = $hash->{NAME};
  1107. return undef if( !defined($hash->{Device}) );
  1108. return undef if( $hash->{sts_expiretime} && $hash->{sts_expiretime} > time - 3600 ); # min 1h
  1109. return undef if( !defined($hash->{IODev}) );
  1110. Log3 $name, 5, "$name: gets3credentials ".$hash->{Device};
  1111. withings_getSessionKey( $hash->{IODev} );
  1112. my ($err,$data) = HttpUtils_BlockingGet({
  1113. url => $hash->{'.https'}."://scalews.withings.com/cgi-bin/v2/device",
  1114. timeout => 10,
  1115. noshutdown => 1,
  1116. data => {callctx => 'foreground', action => 'getsts', deviceid => $hash->{Device}, appname => 'HomeMonitor', apppfm => 'ios' , appliver => '20000', sessionid => $hash->{IODev}->{SessionKey}},
  1117. });
  1118. return undef if(!defined($data));
  1119. my $json = eval { JSON->new->utf8(0)->decode($data) };
  1120. if($@)
  1121. {
  1122. Log3 $name, 2, "$name: json evaluation error on getS3Credentials ".$@;
  1123. return undef;
  1124. }
  1125. Log3 $name, 1, "withings: getS3Credentials json error ".$json->{error} if(defined($json->{error}));
  1126. if(defined($json->{body}{sts}))
  1127. {
  1128. $hash->{sts_region} = $json->{body}{sts}{region};
  1129. $hash->{sts_sessiontoken} = $json->{body}{sts}{sessiontoken};
  1130. $hash->{sts_accesskeyid} = $json->{body}{sts}{accesskeyid};
  1131. $hash->{sts_expiretime} = $json->{body}{sts}{expiretime};
  1132. $hash->{sts_secretaccesskey} = $json->{body}{sts}{secretaccesskey};
  1133. $hash->{sts_buckets} = (@{$json->{body}{sts}{buckets}}).join(",");
  1134. }
  1135. return $json;
  1136. }
  1137. sub withings_signS3Link($$$;$) {
  1138. my ($hash,$url,$sign,$bucket) = @_;
  1139. my $name = $hash->{NAME};
  1140. withings_getS3Credentials($hash);
  1141. my $signing = "GET\n\n\n";
  1142. $signing .= $hash->{sts_expiretime}."\n";
  1143. $signing .= "x-amz-security-token:".$hash->{sts_sessiontoken}."\n";
  1144. $signing .= $sign;
  1145. my $signature = hmac_sha1_base64($signing, $hash->{sts_secretaccesskey})."=";
  1146. $url .= "?AWSAccessKeyId=".uri_escape($hash->{sts_accesskeyid});
  1147. $url .= "&Expires=".$hash->{sts_expiretime};
  1148. $url .= "&x-amz-security-token=".uri_escape($hash->{sts_sessiontoken});
  1149. $url .= "&Signature=".uri_escape($signature);
  1150. return $url;
  1151. }
  1152. sub withings_getUserDetail($) {
  1153. my ($hash) = @_;
  1154. my $name = $hash->{NAME};
  1155. Log3 $name, 5, "$name: getuserdetails ".$hash->{User};
  1156. return undef if( !defined($hash->{User}) );
  1157. return undef if( $hash->{SUBTYPE} ne "USER" );
  1158. return undef if( !defined($hash->{IODev}));
  1159. withings_getSessionKey( $hash->{IODev} );
  1160. my ($err,$data) = HttpUtils_BlockingGet({
  1161. url => $hash->{'.https'}."://scalews.withings.com/index/service/user",
  1162. timeout => 10,
  1163. noshutdown => 1,
  1164. data => {sessionid => $hash->{IODev}->{SessionKey}, userid => $hash->{User} , appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getbyuserid'},
  1165. });
  1166. return undef if(!defined($data));
  1167. my $json = eval { JSON->new->utf8(0)->decode($data) };
  1168. if($@)
  1169. {
  1170. Log3 $name, 2, "$name: json evaluation error on getUserDetail ".$@;
  1171. return undef;
  1172. }
  1173. Log3 $name, 1, "withings: getUserDetail json error ".$json->{error} if(defined($json->{error}));
  1174. return $json->{body}{users}[0];
  1175. }
  1176. sub withings_poll($;$) {
  1177. my ($hash,$force) = @_;
  1178. $force = 0 if(!defined($force));
  1179. my $name = $hash->{NAME};
  1180. RemoveInternalTimer($hash);
  1181. return undef if(IsDisabled($name));
  1182. #my $resolve = inet_aton("scalews.withings.com");
  1183. #if(!defined($resolve))
  1184. #{
  1185. # $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" );
  1186. # InternalTimer( gettimeofday() + 3600, "withings_poll", $hash, 0);
  1187. # return undef;
  1188. #}
  1189. my ($now) = int(time());
  1190. if( $hash->{SUBTYPE} eq "DEVICE" ) {
  1191. my $intervalData = AttrVal($name,"intervalData",900);
  1192. my $intervalDebug = AttrVal($name,"intervalDebug",AttrVal($name,"intervalData",900));
  1193. my $intervalProperties = AttrVal($name,"intervalProperties",AttrVal($name,"intervalData",900));
  1194. my $lastData = ReadingsVal( $name, ".pollData", 0 );
  1195. my $lastDebug = ReadingsVal( $name, ".pollDebug", 0 );
  1196. my $lastProperties = ReadingsVal( $name, ".pollProperties", 0 );
  1197. if(defined($hash->{modelID}) && $hash->{modelID} eq '4') {
  1198. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1199. withings_getDeviceReadingsScale($hash) if($force || $lastData <= ($now - $intervalData));
  1200. }
  1201. elsif(defined($hash->{modelID}) && $hash->{modelID} eq '21') {
  1202. my $intervalAlert = AttrVal($name,"intervalAlert",120);
  1203. my $lastAlert = ReadingsVal( $name, ".pollAlert", 0 );
  1204. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1205. withings_getDeviceEventsBaby($hash) if($force || $lastData <= ($now - $intervalData));
  1206. #withings_getDeviceAlertsBaby($hash) if($force || $lastAlert <= ($now - $intervalAlert));
  1207. }
  1208. elsif(defined($hash->{modelID}) && $hash->{modelID} eq '22') {
  1209. my $intervalAlert = AttrVal($name,"intervalAlert",120);
  1210. my $lastAlert = ReadingsVal( $name, ".pollAlert", 0 );
  1211. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1212. withings_getDeviceReadingsHome($hash) if($force || $lastData <= ($now - $intervalData));
  1213. withings_getDeviceAlertsHome($hash) if($force || $lastAlert <= ($now - $intervalAlert));
  1214. }
  1215. elsif(defined($hash->{typeID}) && $hash->{typeID} eq '16') {
  1216. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1217. withings_getUserReadingsActivity($hash) if($force || $lastData <= ($now - $intervalData));
  1218. }
  1219. elsif(defined($hash->{modelID}) && $hash->{modelID} eq '60') {
  1220. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1221. withings_getDeviceReadingsBedside($hash) if($force || $lastData <= ($now - $intervalData));
  1222. }
  1223. elsif(defined($hash->{modelID}) && ($hash->{modelID} eq '61' || $hash->{modelID} eq '62' || $hash->{modelID} eq '63')) {
  1224. withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties));
  1225. withings_getUserReadingsSleep($hash) if($force || $lastData <= ($now - $intervalData));
  1226. withings_getUserReadingsSleepDebug($hash) if($force || $lastDebug <= ($now - $intervalDebug));
  1227. }
  1228. else
  1229. {
  1230. withings_getDeviceProperties($hash) if($force || $lastProperties <= ($now - $intervalProperties));
  1231. }
  1232. } elsif( $hash->{SUBTYPE} eq "DUMMY" ) {
  1233. my $intervalData = AttrVal($name,"intervalData",900);
  1234. my $lastData = ReadingsVal( $name, ".pollData", 0 );
  1235. if($hash->{typeID} eq '16') {
  1236. withings_getUserReadingsActivity($hash) if($force || $lastData <= ($now - $intervalData));
  1237. }
  1238. } elsif( $hash->{SUBTYPE} eq "USER" ) {
  1239. my $intervalData = AttrVal($name,"intervalData",900);
  1240. my $intervalDaily = AttrVal($name,"intervalDaily",(6*60*60));
  1241. my $lastData = ReadingsVal( $name, ".pollData", 0 );
  1242. my $lastDaily = ReadingsVal( $name, ".pollDaily", 0 );
  1243. withings_getUserReadingsCommon($hash) if($force || $lastData <= ($now - $intervalData));
  1244. withings_getUserReadingsDaily($hash) if($force || $lastDaily <= ($now - $intervalDaily));
  1245. }
  1246. InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0);
  1247. }
  1248. sub withings_getUserReadingsDaily($) {
  1249. my ($hash) = @_;
  1250. my $name = $hash->{NAME};
  1251. Log3 $name, 5, "$name: getuserdailystats ".$hash->{User};
  1252. return undef if( !defined($hash->{IODev}) );
  1253. withings_getSessionKey( $hash->{IODev} );
  1254. my ($now) = time;
  1255. my $lastupdate = ReadingsVal( $name, ".lastAggregate", ($now-21*24*60*60) );#$hash->{created} );#
  1256. my $enddate = ($lastupdate+(14*24*60*60));
  1257. $enddate = $now if ($enddate > $now);
  1258. my $startdateymd = strftime("%Y-%m-%d", localtime($lastupdate));
  1259. my $enddateymd = strftime("%Y-%m-%d", localtime($enddate));
  1260. HttpUtils_NonblockingGet({
  1261. url => "https://scalews.withings.com/cgi-bin/v2/aggregate",
  1262. timeout => 60,
  1263. noshutdown => 1,
  1264. data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, range => '1', meastype => '36,37,38,40,41,49,50,51,52,53,87', startdateymd => $startdateymd, enddateymd => $enddateymd, appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getbyuserid'},
  1265. hash => $hash,
  1266. type => 'userDailyAggregate',
  1267. enddate => int($enddate),
  1268. callback => \&withings_Dispatch,
  1269. });
  1270. $lastupdate = ReadingsVal( $name, ".lastActivity", ($now-21*24*60*60) );#$hash->{created} );
  1271. $enddate = ($lastupdate+(14*24*60*60));
  1272. $enddate = $now if ($enddate > $now);
  1273. $startdateymd = strftime("%Y-%m-%d", localtime($lastupdate));
  1274. $enddateymd = strftime("%Y-%m-%d", localtime($enddate));
  1275. HttpUtils_NonblockingGet({
  1276. url => "https://scalews.withings.com/cgi-bin/v2/activity",
  1277. timeout => 60,
  1278. noshutdown => 1,
  1279. data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, subcategory => '37', startdateymd => $startdateymd, enddateymd => $enddateymd, appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getbyuserid'},
  1280. hash => $hash,
  1281. type => 'userDailyActivity',
  1282. enddate => int($enddate),
  1283. callback => \&withings_Dispatch,
  1284. });
  1285. # HttpUtils_NonblockingGet({
  1286. # url => "https://scalews.withings.com/cgi-bin/v2/activity",
  1287. # timeout => 60,
  1288. # noshutdown => 1,
  1289. # data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, startdateymd => $startdateymd, enddateymd => $enddateymd, appname => 'hmw', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getbyuserid'},
  1290. # hash => $hash,
  1291. # type => 'userDailyActivity',
  1292. # enddate => int($enddate),
  1293. # callback => \&withings_Dispatch,
  1294. # });
  1295. my ($seconds) = gettimeofday();
  1296. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1297. readingsSingleUpdate( $hash, ".pollDaily", $seconds, 0 );
  1298. return undef;
  1299. }
  1300. sub withings_getUserReadingsCommon($) {
  1301. my ($hash) = @_;
  1302. my $name = $hash->{NAME};
  1303. Log3 $name, 5, "$name: getuserreadings ".$hash->{User};
  1304. return undef if( !defined($hash->{IODev}) );
  1305. withings_getSessionKey( $hash->{IODev} );
  1306. my ($now) = time;
  1307. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-100*24*60*60) );#$hash->{created} );#
  1308. my $enddate = ($lastupdate+(100*24*60*60));
  1309. $enddate = $now if ($enddate > $now);
  1310. HttpUtils_NonblockingGet({
  1311. url => "https://scalews.withings.com/cgi-bin/measure",
  1312. timeout => 60,
  1313. noshutdown => 1,
  1314. data => {sessionid => $hash->{IODev}->{SessionKey}, category => '1', userid=> $hash->{User}, offset => '0', limit => '400', startdate => int($lastupdate), enddate => int($enddate), appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getmeas'},
  1315. hash => $hash,
  1316. type => 'userReadingsCommon',
  1317. enddate => int($enddate),
  1318. callback => \&withings_Dispatch,
  1319. });
  1320. my ($seconds) = gettimeofday();
  1321. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1322. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  1323. return undef;
  1324. }
  1325. sub withings_getUserReadingsSleep($) {
  1326. my ($hash) = @_;
  1327. my $name = $hash->{NAME};
  1328. Log3 $name, 5, "$name: getsleepreadings ".$hash->{User};
  1329. return undef if( !defined($hash->{IODev}) );
  1330. withings_getSessionKey( $hash->{IODev} );
  1331. my ($now) = time;
  1332. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  1333. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1334. my $enddate = ($lastupdate+(8*60*60));
  1335. $enddate = $now if ($enddate > $now);
  1336. # data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, meastype => '43,44,11,57,59,60,61,62,63,64,65,66,67,68,69,70', startdate => int($lastupdate), enddate => int($enddate), devicetype => '32', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'},
  1337. HttpUtils_NonblockingGet({
  1338. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  1339. timeout => 60,
  1340. noshutdown => 1,
  1341. data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, meastype => '11,39,41,43,44,57,59,87', startdate => int($lastupdate), enddate => int($enddate), devicetype => '32', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'},
  1342. hash => $hash,
  1343. type => 'userReadingsSleep',
  1344. enddate => int($enddate),
  1345. callback => \&withings_Dispatch,
  1346. });
  1347. my ($seconds) = gettimeofday();
  1348. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1349. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  1350. return undef;
  1351. }
  1352. sub withings_getUserReadingsSleepDebug($) {
  1353. my ($hash) = @_;
  1354. my $name = $hash->{NAME};
  1355. Log3 $name, 5, "$name: getsleepreadingsdebug ".$hash->{User};
  1356. return undef if( !defined($hash->{IODev}) );
  1357. withings_getSessionKey( $hash->{IODev} );
  1358. my ($now) = time;
  1359. my $lastupdate = ReadingsVal( $name, ".lastDebug", ($now-7*24*60*60) );#$hash->{created} );
  1360. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1361. my $enddate = ($lastupdate+(8*60*60));
  1362. $enddate = $now if ($enddate > $now);
  1363. HttpUtils_NonblockingGet({
  1364. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  1365. timeout => 60,
  1366. noshutdown => 1,
  1367. data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, meastype => '60,61,62,63,64,65,66,67,68,69,70', startdate => int($lastupdate), enddate => int($enddate), devicetype => '32', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'},
  1368. hash => $hash,
  1369. type => 'userReadingsSleepDebug',
  1370. enddate => int($enddate),
  1371. callback => \&withings_Dispatch,
  1372. });
  1373. my ($seconds) = gettimeofday();
  1374. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1375. readingsSingleUpdate( $hash, ".pollDebug", $seconds, 0 );
  1376. return undef;
  1377. }
  1378. sub withings_getUserReadingsActivity($) {
  1379. my ($hash) = @_;
  1380. my $name = $hash->{NAME};
  1381. Log3 $name, 5, "$name: getactivityreadings ".$hash->{User};
  1382. return undef if( !defined($hash->{IODev}) );
  1383. withings_getSessionKey( $hash->{IODev} );
  1384. my ($now) = time;
  1385. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );#
  1386. $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate);
  1387. my $enddate = ($lastupdate+(8*60*60));
  1388. $enddate = $now if ($enddate > $now);
  1389. Log3 $name, 5, "$name: getactivityreadings ".$lastupdate." to ".$enddate;
  1390. HttpUtils_NonblockingGet({
  1391. url => "https://scalews.withings.com/cgi-bin/v2/measure",
  1392. timeout => 60,
  1393. noshutdown => 1,
  1394. data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, meastype => '36,37,38,39,40,41,42,43,44,59,70,87,90', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'},
  1395. hash => $hash,
  1396. type => 'userReadingsActivity',
  1397. enddate => int($enddate),
  1398. callback => \&withings_Dispatch,
  1399. });
  1400. my ($seconds) = gettimeofday();
  1401. $hash->{LAST_POLL} = FmtDateTime( $seconds );
  1402. readingsSingleUpdate( $hash, ".pollData", $seconds, 0 );
  1403. return undef;
  1404. }
  1405. sub withings_parseProperties($$) {
  1406. my ($hash,$json) = @_;
  1407. my $name = $hash->{NAME};
  1408. Log3 $name, 5, "$name: parsedevice";
  1409. #parse
  1410. my $detail = $json->{body};
  1411. readingsBeginUpdate($hash);
  1412. if( defined($detail->{batterylvl}) and $detail->{batterylvl} > 0 and $detail->{type} ne '32' and $detail->{model} ne '22') {
  1413. readingsBulkUpdate( $hash, "batteryPercent", $detail->{batterylvl}, 1 );
  1414. readingsBulkUpdate( $hash, "batteryState", ($detail->{batterylvl}>20?"ok":"low"), 1 );
  1415. }
  1416. readingsBulkUpdate( $hash, "lastWeighinDate", FmtDateTime($detail->{lastweighindate}), 1 ) if( defined($detail->{lastweighindate}) and $detail->{lastweighindate} > 0 and $detail->{model} ne '60' );
  1417. readingsBulkUpdate( $hash, "lastSessionDate", FmtDateTime($detail->{lastsessiondate}), 1 ) if( defined($detail->{lastsessiondate}) );
  1418. $hash->{lastsessiondate} = $detail->{lastsessiondate} if( defined($detail->{lastsessiondate}) );
  1419. readingsEndUpdate($hash,1);
  1420. }
  1421. sub withings_parseMeasureGroups($$) {
  1422. my ($hash, $json) = @_;
  1423. my $name = $hash->{NAME};
  1424. #parse
  1425. Log3 $name, 5, "$name: parsemeasuregroups";
  1426. my ($now) = int(time);
  1427. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-21*24*60*60) );
  1428. my $newlastupdate = $lastupdate;
  1429. $hash->{status} = $json->{status};
  1430. if( $hash->{status} == 0 ) {
  1431. my $i = 0;
  1432. foreach my $measuregrp ( sort { $a->{date} <=> $b->{date} } @{$json->{body}{measuregrps}}) {
  1433. if( $measuregrp->{date} < $newlastupdate )
  1434. {
  1435. Log3 $name, 4, "$name: old measuregroup skipped: ".FmtDateTime($measuregrp->{date});
  1436. next;
  1437. }
  1438. $newlastupdate = $measuregrp->{date};
  1439. foreach my $measure (@{$measuregrp->{measures}}) {
  1440. my $reading = $measure_types{$measure->{type}}->{reading};
  1441. if( !defined($reading) ) {
  1442. Log3 $name, 1, "$name: unknown measure type: $measure->{type}";
  1443. next;
  1444. }
  1445. my $value = $measure->{value} * 10 ** $measure->{unit};
  1446. readingsBeginUpdate($hash);
  1447. $hash->{".updateTimestamp"} = FmtDateTime($measuregrp->{date});
  1448. readingsBulkUpdate( $hash, $reading, $value, 1 );
  1449. $hash->{CHANGETIME}[0] = FmtDateTime($measuregrp->{date});
  1450. readingsEndUpdate($hash,1);
  1451. $i++;
  1452. }
  1453. }
  1454. if($newlastupdate == $lastupdate and $i == 0)
  1455. {
  1456. my $user = withings_getUserDetail( $hash );
  1457. $hash->{modified} = $user->{modified};
  1458. $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate});
  1459. $newlastupdate = $user->{modified} if($user->{modified} and $user->{modified} < $newlastupdate);
  1460. }
  1461. $newlastupdate = $now if($newlastupdate > $now);
  1462. if($newlastupdate < $lastupdate-1)
  1463. {
  1464. Log3 $name, 2, "$name: Measuregroups gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1465. withings_getDeviceProperties($hash) if($i>0);
  1466. $newlastupdate = $lastupdate-1;
  1467. }
  1468. $hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1469. $newlastupdate = int(time) if($newlastupdate > (time+3600));
  1470. readingsSingleUpdate( $hash, ".lastData", $newlastupdate+1, 0 );
  1471. delete $hash->{CHANGETIME};
  1472. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from MeasureGroups (latest: '.FmtDateTime($newlastupdate).')';
  1473. }
  1474. }
  1475. sub withings_parseMeasurements($$) {
  1476. my ($hash, $json) = @_;
  1477. my $name = $hash->{NAME};
  1478. #parse
  1479. Log3 $name, 4, "$name: parsemeasurements";
  1480. my ($now) = time;
  1481. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-21*24*60*60) );
  1482. my $newlastupdate = $lastupdate;
  1483. my $i = 0;
  1484. if( $json )
  1485. {
  1486. $hash->{status} = $json->{status};
  1487. my @readings = ();
  1488. if( $hash->{status} == 0 )
  1489. {
  1490. foreach my $series ( @{$json->{body}{series}}) {
  1491. my $reading = $measure_types{$series->{type}}->{reading};
  1492. if( !defined($reading) ) {
  1493. Log3 $name, 1, "$name: unknown measure type: $series->{type}";
  1494. next;
  1495. }
  1496. foreach my $measure (@{$series->{data}}) {
  1497. my $value = $measure->{value};
  1498. push(@readings, [$measure->{date}, $reading, $value]);
  1499. }
  1500. }
  1501. if( @readings ) {
  1502. $i = 0;
  1503. foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings) {
  1504. if( $reading->[0] < $newlastupdate )
  1505. {
  1506. Log3 $name, 5, "$name: old measurement skipped: ".FmtDateTime($reading->[0])." ".$reading->[1];
  1507. next;
  1508. }
  1509. $newlastupdate = $reading->[0];
  1510. readingsBeginUpdate($hash);
  1511. $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]);
  1512. readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 );
  1513. $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]);;
  1514. readingsEndUpdate($hash,1);
  1515. $i++;
  1516. }
  1517. }
  1518. if($newlastupdate == $lastupdate and $i == 0)
  1519. {
  1520. my $device = withings_getDeviceDetail( $hash );
  1521. $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate});
  1522. $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate);
  1523. $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate);
  1524. }
  1525. $newlastupdate = $now if($newlastupdate > $now);
  1526. if($newlastupdate < $lastupdate-1)
  1527. {
  1528. Log3 $name, 2, "$name: Measurements gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1529. withings_getDeviceProperties($hash) if($i>0);
  1530. $newlastupdate = $lastupdate-1;
  1531. }
  1532. $hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1533. $newlastupdate = int(time) if($newlastupdate > (time+3600));
  1534. readingsSingleUpdate( $hash, ".lastData", $newlastupdate+1, 0 );
  1535. delete $hash->{CHANGETIME};
  1536. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Measurements (latest: '.FmtDateTime($newlastupdate).')';
  1537. }
  1538. }
  1539. }
  1540. sub withings_parseAggregate($$) {
  1541. my ($hash, $json) = @_;
  1542. my $name = $hash->{NAME};
  1543. #parse
  1544. Log3 $name, 5, "$name: parseaggregate";
  1545. #return undef;
  1546. my ($now) = time;
  1547. my $lastupdate = ReadingsVal( $name, ".lastAggregate", ($now-21*24*60*60) );
  1548. my $newlastupdate = $lastupdate;
  1549. my $i = 0;
  1550. my $unfinished;
  1551. if( $json )
  1552. {
  1553. $hash->{status} = $json->{status};
  1554. my @readings = ();
  1555. if( $hash->{status} == 0 )
  1556. {
  1557. if(defined($json->{body}{series}))
  1558. {
  1559. my $series = $json->{body}->{series};
  1560. foreach my $serieskey ( keys %$series)
  1561. {
  1562. if(defined($series->{$serieskey}))
  1563. {
  1564. my $typestring = substr($serieskey, -2);
  1565. my $serieshash = $json->{body}->{series}{$serieskey};
  1566. next if(ref($serieshash) ne "HASH");
  1567. foreach my $daykey ( keys %$serieshash)
  1568. {
  1569. my $dayhash = $json->{body}->{series}{$serieskey}{$daykey};
  1570. next if(ref($dayhash) ne "HASH");
  1571. if(!$dayhash->{complete})
  1572. {
  1573. $unfinished = 1;
  1574. next;
  1575. }
  1576. my ($year,$mon,$day) = split(/[\s-]+/, $daykey);
  1577. my $timestamp = timelocal(0,0,18,$day,$mon-1,$year-1900);
  1578. #my $timestamp = $dayhash->{midnight};
  1579. my $reading = $measure_types{$typestring}->{dailyreading};
  1580. if( !defined($reading) ) {
  1581. Log3 $name, 1, "$name: unknown measure type: $typestring";
  1582. next;
  1583. }
  1584. my $value = $dayhash->{sum};
  1585. push(@readings, [$timestamp, $reading, $value]);
  1586. }
  1587. }
  1588. }
  1589. }
  1590. if( @readings )
  1591. {
  1592. $i = 0;
  1593. foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings)
  1594. {
  1595. if( $reading->[0] < $newlastupdate )
  1596. {
  1597. Log3 $name, 5, "$name: old aggregate skipped: ".FmtDateTime($reading->[0])." ".$reading->[1];
  1598. next;
  1599. }
  1600. $newlastupdate = $reading->[0];
  1601. readingsBeginUpdate($hash);
  1602. $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]);
  1603. readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 );
  1604. $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]);
  1605. readingsEndUpdate($hash,1);
  1606. $i++;
  1607. }
  1608. }
  1609. if($newlastupdate == $lastupdate and $i == 0)
  1610. {
  1611. $newlastupdate = $lastupdate - 1; #$json->{requestedenddate} if($json->{requestedenddate});
  1612. }
  1613. $newlastupdate = $now if($newlastupdate > $now);
  1614. if($newlastupdate < $lastupdate-1)
  1615. {
  1616. Log3 $name, 2, "$name: Aggregate gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1617. withings_getDeviceProperties($hash) if($i>0);
  1618. $newlastupdate = $lastupdate-1;
  1619. }
  1620. readingsSingleUpdate( $hash, ".lastAggregate", $newlastupdate+1, 0 );
  1621. #$hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1622. delete $hash->{CHANGETIME};
  1623. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Aggregate (latest: '.FmtDateTime($newlastupdate).')';
  1624. }
  1625. }
  1626. }
  1627. sub withings_parseActivity($$) {
  1628. my ($hash, $json) = @_;
  1629. my $name = $hash->{NAME};
  1630. #parse
  1631. Log3 $name, 5, "$name: parseactivity";
  1632. my ($now) = time;
  1633. my $lastupdate = ReadingsVal( $name, ".lastActivity", ($now-21*24*60*60) );
  1634. my $newlastupdate = $lastupdate;
  1635. my $i = 0;
  1636. my $unfinished;
  1637. if( $json )
  1638. {
  1639. $hash->{status} = $json->{status};
  1640. my @readings = ();
  1641. if( $hash->{status} == 0 )
  1642. {
  1643. foreach my $series ( @{$json->{body}{series}})
  1644. {
  1645. if($series->{completed} ne '1')
  1646. {
  1647. $unfinished = 1;
  1648. next;
  1649. }
  1650. foreach my $dataset ( keys (%{$series->{data}}))
  1651. {
  1652. if(!defined($sleep_readings{$dataset}->{reading}))
  1653. {
  1654. Log3 $name, 2, "$name: unknown sleep reading $dataset";
  1655. next;
  1656. }
  1657. my ($year,$mon,$day) = split(/[\s-]+/, $series->{date});
  1658. my $timestamp = timelocal(0,0,6,$day,$mon-1,$year-1900);
  1659. my $reading = $sleep_readings{$dataset}->{reading};
  1660. my $value = $series->{data}{$dataset};
  1661. push(@readings, [$timestamp, $reading, $value]);
  1662. }
  1663. }
  1664. if( @readings ) {
  1665. $i = 0;
  1666. foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings) {
  1667. if( $reading->[0] < $newlastupdate )
  1668. {
  1669. Log3 $name, 5, "$name: old activity skipped: ".FmtDateTime($reading->[0])." ".$reading->[1];
  1670. next;
  1671. }
  1672. $newlastupdate = $reading->[0];
  1673. readingsBeginUpdate($hash);
  1674. $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]);
  1675. readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 );
  1676. $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]);
  1677. readingsEndUpdate($hash,1);
  1678. $i++;
  1679. }
  1680. }
  1681. if($newlastupdate == $lastupdate and $i == 0)
  1682. {
  1683. $newlastupdate = $lastupdate - 1; #$json->{requestedenddate} if($json->{requestedenddate});
  1684. }
  1685. $newlastupdate = $now if($newlastupdate > $now);
  1686. if($newlastupdate < $lastupdate-1)
  1687. {
  1688. Log3 $name, 2, "$name: Activity gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") .$i if($i>0)";
  1689. withings_getDeviceProperties($hash) if($i>0);
  1690. $newlastupdate = $lastupdate-1;
  1691. }
  1692. readingsSingleUpdate( $hash, ".lastActivity", $newlastupdate+1, 0 );
  1693. #$hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1694. delete $hash->{CHANGETIME};
  1695. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Activity (latest: '.FmtDateTime($newlastupdate).')';
  1696. }
  1697. }
  1698. }
  1699. sub withings_parseWorkouts($$) {
  1700. my ($hash, $json) = @_;
  1701. my $name = $hash->{NAME};
  1702. #parse
  1703. Log3 $name, 1, "$name: parseworkouts\n".Dumper($json);
  1704. return undef;
  1705. }
  1706. sub withings_parseVasistas($$;$) {
  1707. my ($hash, $json, $datatype) = @_;
  1708. my $name = $hash->{NAME};
  1709. #parse
  1710. Log3 $name, 5, "$name: parsevasistas";
  1711. my ($now) = time;
  1712. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-21*24*60*60) );
  1713. $lastupdate = ReadingsVal( $name, ".lastDebug", ($now-21*24*60*60) ) if($datatype =~ /Debug/);
  1714. if( $json ) {
  1715. $hash->{status} = $json->{status};
  1716. if( $hash->{status} == 0 ) {
  1717. my @readings = ();
  1718. my $i = 0;
  1719. my $j;
  1720. my $k;
  1721. my $readingsdate;
  1722. my $newlastupdate = $lastupdate;
  1723. my $iscurrent = 0;
  1724. foreach my $series ( @{$json->{body}{series}}) {
  1725. $j=0;
  1726. my @types= (@{$series->{types}});
  1727. my @dates= (@{$series->{dates}});
  1728. my @values= (@{$series->{vasistas}});
  1729. foreach $readingsdate (@dates) {
  1730. my @readingsvalue = (@{$values[$j++]});
  1731. if($readingsdate <= $lastupdate)
  1732. {
  1733. Log3 $name, 5, "$name: old vasistas skipped: ".FmtDateTime($readingsdate);
  1734. next;
  1735. }
  1736. $k=0;
  1737. foreach my $readingstype (@types) {
  1738. my $updatetime = FmtDateTime($readingsdate);
  1739. my $updatevalue = $readingsvalue[$k++];
  1740. my $updatetype = $measure_types{$readingstype}->{reading};
  1741. if( !defined($updatetype) ) {
  1742. Log3 $name, 1, "$name: unknown measure type: $readingstype";
  1743. next;
  1744. }
  1745. if(($updatetype eq "breathing") and ($updatevalue > 90)) {
  1746. Log3 $name, 2, "$name: Implausible Aura reading ".$updatetime.' '.$updatetype.': '.$updatevalue;
  1747. $newlastupdate = $readingsdate if($readingsdate > $newlastupdate);
  1748. next;
  1749. }
  1750. if($updatetype eq "duration")
  1751. {
  1752. Log3 $name, 4, "$name: Duration skipped ".$updatetime.' '.$updatetype.': '.$updatevalue if($updatevalue > 90);
  1753. $newlastupdate = $readingsdate if($readingsdate > $newlastupdate);
  1754. next;
  1755. }
  1756. if($updatetype eq "activityType")
  1757. {
  1758. my $activity = $updatevalue;
  1759. $updatevalue = $activity_types{$updatevalue};
  1760. if( !defined($updatevalue) ) {
  1761. Log3 $name, 1, "$name: unknown activity type: $activity";
  1762. $updatevalue = $activity;
  1763. }
  1764. }
  1765. readingsBeginUpdate($hash);
  1766. $hash->{".updateTimestamp"} = FmtDateTime($readingsdate);
  1767. readingsBulkUpdate( $hash, $updatetype, $updatevalue, 1 );
  1768. $hash->{CHANGETIME}[0] = FmtDateTime($readingsdate);
  1769. readingsEndUpdate($hash,1);
  1770. if($updatetype ne "unknown") {
  1771. $newlastupdate = $readingsdate if($readingsdate > $newlastupdate);
  1772. $i++;
  1773. }
  1774. #start in-bed detection
  1775. if($iscurrent == 0 && $datatype =~ /Sleep/){
  1776. if($i>40 && $readingsdate > time()-3600){
  1777. $iscurrent = 1;
  1778. #Log3 $name, 1, "$name: in-bed: ".FmtDateTime($readingsdate) if($i>0);
  1779. readingsBeginUpdate($hash);
  1780. $hash->{".updateTimestamp"} = FmtDateTime($readingsdate);
  1781. readingsBulkUpdate( $hash, "in_bed", 1, 1 );
  1782. $hash->{CHANGETIME}[0] = FmtDateTime($readingsdate);
  1783. readingsEndUpdate($hash,1);
  1784. }
  1785. }
  1786. #end in-bed detection
  1787. }
  1788. }
  1789. }
  1790. if($newlastupdate == $lastupdate and $i == 0)
  1791. {
  1792. my $device = withings_getDeviceDetail( $hash );
  1793. $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate});
  1794. $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate);
  1795. $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate);
  1796. #start in-bed detection
  1797. if($datatype =~ /Sleep/ && $iscurrent == 0){
  1798. if($device->{lastweighindate} > (time()-1800)){
  1799. readingsSingleUpdate( $hash, "in_bed", 1, 1 );
  1800. } else {
  1801. readingsSingleUpdate( $hash, "in_bed", 0, 1 );
  1802. }
  1803. }
  1804. #end in-bed detection
  1805. }
  1806. $newlastupdate = $now if($newlastupdate > $now);
  1807. if($newlastupdate < ($lastupdate-1))
  1808. {
  1809. Log3 $name, 2, "$name: Vasistas gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1810. withings_getDeviceProperties($hash) if($i>0);
  1811. $newlastupdate = $lastupdate;
  1812. }
  1813. my ($seconds) = gettimeofday();
  1814. $hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1815. $newlastupdate = int(time) if($newlastupdate > (time+3600));
  1816. if($datatype =~ /Debug/)
  1817. {
  1818. readingsSingleUpdate( $hash, ".lastDebug", $newlastupdate, 0 );
  1819. } else {
  1820. readingsSingleUpdate( $hash, ".lastData", $newlastupdate, 0 );
  1821. }
  1822. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Vasistas (latest: '.FmtDateTime($newlastupdate).')';
  1823. }
  1824. }
  1825. }
  1826. sub withings_parseTimeline($$) {
  1827. my ($hash, $json) = @_;
  1828. my $name = $hash->{NAME};
  1829. #parse
  1830. Log3 $name, 5, "$name: parsemetimeline ";
  1831. my ($now) = time;
  1832. my $lastupdate = ReadingsVal( $name, ".lastAlert", ($now-21*24*60*60) );
  1833. my $newlastupdate = $lastupdate;
  1834. $hash->{status} = $json->{status};
  1835. if( $hash->{status} == 0 )
  1836. {
  1837. my $i = 0;
  1838. foreach my $event ( sort { $a->{epoch} <=> $b->{epoch} } @{$json->{body}{timeline}}) {
  1839. if( $event->{epoch} < $newlastupdate )
  1840. {
  1841. Log3 $name, 5, "$name: old timeline event skipped: ".FmtDateTime($event->{epoch})." $event->{class}";
  1842. next;
  1843. }
  1844. $newlastupdate = $event->{epoch};
  1845. if($event->{class} eq 'period_activity' or $event->{class} eq 'period_activity_start' or $event->{class} eq 'period_activity_cancel' or $event->{class} eq 'period_offline')
  1846. {
  1847. next;
  1848. }
  1849. elsif($event->{class} eq 'deleted')
  1850. {
  1851. Log3 $name, 5, "withings: event " . FmtDateTime($event->{epoch})." Event was deleted";
  1852. next;
  1853. }
  1854. elsif($event->{class} ne 'noise_detected' && $event->{class} ne 'movement_detected' && $event->{class} ne 'alert_environment' && $event->{class} ne 'offline' && $event->{class} ne 'online' && $event->{class} ne 'snapshot')
  1855. {
  1856. Log3 $name, 2, "withings: alert class unknown " . $event->{class};
  1857. next;
  1858. }
  1859. my $reading = $timeline_classes{$event->{class}}->{reading};
  1860. my $value = "alert";
  1861. $value = $event->{data}->{value} * 10 ** $timeline_classes{$event->{class}}->{unit} if(defined($event->{class}) && defined($event->{data}) && defined($event->{data}->{value}) && defined($timeline_classes{$event->{class}}) && defined($timeline_classes{$event->{class}}->{unit}));
  1862. if( !defined($reading) ) {
  1863. Log3 $name, 2, "$name: unknown event type: $event->{class}";
  1864. next;
  1865. }
  1866. else
  1867. {
  1868. readingsBeginUpdate($hash);
  1869. $hash->{".updateTimestamp"} = FmtDateTime($event->{epoch});
  1870. readingsBulkUpdate( $hash, $reading, $value, 1 );
  1871. $hash->{CHANGETIME}[0] = FmtDateTime($event->{epoch});
  1872. readingsEndUpdate($hash,1);
  1873. $i++;
  1874. }
  1875. if(AttrVal($name,"videoLinkEvents",0) eq "1")
  1876. {
  1877. my $pathlist = $event->{data}->{path_list}[0];
  1878. my $eventurl = withings_signS3Link($hash,$pathlist->{url},$pathlist->{sign});
  1879. DoTrigger($name, "alerturl: ".$eventurl);
  1880. }
  1881. }
  1882. if($newlastupdate == $lastupdate and $i == 0)
  1883. {
  1884. my $device = withings_getDeviceDetail( $hash );
  1885. $newlastupdate = $json->{requestedenddate} if(defined($json->{requestedenddate}));
  1886. $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate);
  1887. $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate);
  1888. }
  1889. $newlastupdate = $now if($newlastupdate > $now);
  1890. if($newlastupdate < $lastupdate-1)
  1891. {
  1892. Log3 $name, 2, "$name: Timeline gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1893. withings_getDeviceProperties($hash) if($i>0);
  1894. $newlastupdate = $lastupdate-1;
  1895. }
  1896. readingsSingleUpdate( $hash, ".lastAlert", $newlastupdate+1, 0 );
  1897. #$hash->{LAST_DATA} = FmtDateTime( $lastupdate );
  1898. delete $hash->{CHANGETIME};
  1899. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Timeline (latest: '.FmtDateTime($newlastupdate).')';
  1900. }
  1901. }
  1902. sub withings_parseEvents($$) {
  1903. my ($hash, $json) = @_;
  1904. my $name = $hash->{NAME};
  1905. #parse
  1906. Log3 $name, 5, "$name: parseevents";
  1907. my ($now) = time;
  1908. my $lastupdate = ReadingsVal( $name, ".lastData", ($now-21*24*60*60) );
  1909. my $lastalertupdate = ReadingsVal( $name, ".lastAlert", ($now-21*24*60*60) );
  1910. my $newlastupdate = $lastupdate;
  1911. $hash->{status} = $json->{status};
  1912. if( $hash->{status} == 0 ) {
  1913. my $i = 0;
  1914. foreach my $event ( sort { $a->{date} <=> $b->{date} } @{$json->{body}{events}}) {
  1915. if( $event->{date} < $newlastupdate )
  1916. {
  1917. Log3 $name, 5, "$name: old event skipped: ".FmtDateTime($event->{date})." $event->{type}";
  1918. next;
  1919. }
  1920. next if( $event->{deviceid} ne $hash->{Device} );
  1921. $newlastupdate = $event->{date};
  1922. readingsBeginUpdate($hash);
  1923. $hash->{".updateTimestamp"} = FmtDateTime($event->{date});
  1924. my $changeindex = 0;
  1925. #Log3 $name, 5, "withings: event " . FmtDateTime($event->{date})." ".$event->{type}." ".$event->{activated}."/".$event->{measure}{value};
  1926. my $reading = $event_types{$event->{type}}->{reading};
  1927. my $value = "notice";
  1928. if($event->{activated})
  1929. {
  1930. $lastalertupdate = $event->{date};
  1931. $value = "alert";
  1932. }
  1933. if( !defined($reading) ) {
  1934. Log3 $name, 2, "$name: unknown event type: $event->{type}";
  1935. next;
  1936. }
  1937. else
  1938. {
  1939. readingsBulkUpdate( $hash, $reading, $value, 1 );
  1940. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date});
  1941. }
  1942. if(defined($event->{duration}) and $event->{duration} ne "0")
  1943. {
  1944. my $durationreading = $event_types{$event->{type}}->{duration};
  1945. my $durationvalue = $event->{duration};
  1946. readingsBulkUpdate( $hash, $durationreading, $durationvalue, 0 );
  1947. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date});
  1948. }
  1949. if($event->{type} ne "20" and $event->{activated})
  1950. {
  1951. my $thresholdreading = $event_types{$event->{type}}->{threshold};
  1952. my $thresholdvalue = $event->{threshold}->{value} * 10 ** $event_types{$event->{type}}->{unit};
  1953. readingsBulkUpdate( $hash, $thresholdreading, $thresholdvalue, 0 );
  1954. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date});
  1955. }
  1956. readingsEndUpdate($hash,1);
  1957. $i++;
  1958. }
  1959. if($newlastupdate == $lastupdate and $i == 0)
  1960. {
  1961. my $device = withings_getDeviceDetail( $hash );
  1962. $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate});
  1963. $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate);
  1964. $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate);
  1965. }
  1966. $newlastupdate = $now if($newlastupdate > $now);
  1967. if($newlastupdate < $lastupdate-1)
  1968. {
  1969. Log3 $name, 2, "$name: Events gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0);
  1970. withings_getDeviceProperties($hash) if($i>0);
  1971. $newlastupdate = $lastupdate-1;
  1972. }
  1973. $hash->{LAST_DATA} = FmtDateTime( $newlastupdate );
  1974. $newlastupdate = int(time) if($newlastupdate > (time+3600));
  1975. $lastalertupdate = int(time) if($lastalertupdate > (time+3600));
  1976. readingsBeginUpdate($hash);
  1977. readingsBulkUpdate( $hash, ".lastAlert", $lastalertupdate, 0 );
  1978. readingsBulkUpdate( $hash, ".lastData", $newlastupdate+1, 0 );
  1979. readingsEndUpdate($hash,0);
  1980. delete $hash->{CHANGETIME};
  1981. Log3 $name, (($i>0)?3:4), "$name: got ".$i.' entries from Events (latest: '.FmtDateTime($newlastupdate).')';
  1982. }
  1983. }
  1984. sub withings_Get($$@) {
  1985. my ($hash, $name, $cmd) = @_;
  1986. my $list;
  1987. if( $hash->{SUBTYPE} eq "USER" ) {
  1988. $list = "update:noArg updateAll:noArg";
  1989. if( $cmd eq "updateAll" ) {
  1990. withings_poll($hash,2);
  1991. return undef;
  1992. }
  1993. elsif( $cmd eq "update" ) {
  1994. withings_poll($hash,1);
  1995. return undef;
  1996. }
  1997. } elsif( $hash->{SUBTYPE} eq "DEVICE" || $hash->{SUBTYPE} eq "DUMMY" ) {
  1998. $list = "update:noArg updateAll:noArg";
  1999. $list .= " videoLink:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '21');
  2000. $list .= " videoCredentials:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '22');
  2001. $list .= " settings:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '60' && AttrVal($name,"IP",undef));
  2002. if( $cmd eq "videoCredentials" ) {
  2003. my $credentials = withings_getS3Credentials($hash);
  2004. return undef;
  2005. }
  2006. elsif( $cmd eq "videoLink" ) {
  2007. my $ret = "Flash Player Links:\n";
  2008. my $videolinkdata = withings_getVideoLink($hash);
  2009. if(defined($videolinkdata->{body}{device}))
  2010. {
  2011. #$hash->{videolink_ext} = "http://fpdownload.adobe.com/strobe/FlashMediaPlayback_101.swf?streamType=live&autoPlay=true&playButtonOverlay=false&src=rtmp://".$videolinkdata->{body}{device}{proxy_ip}.":".$videolinkdata->{body}{device}{proxy_port}."/".$videolinkdata->{body}{device}{kp_hash}."/";
  2012. #$hash->{videolink_int} = "http://fpdownload.adobe.com/strobe/FlashMediaPlayback_101.swf?streamType=live&autoPlay=true&playButtonOverlay=false&src=rtmp://".$videolinkdata->{body}{device}{private_ip}.":".$videolinkdata->{body}{device}{proxy_port}."/".$videolinkdata->{body}{device}{kd_hash}."/";
  2013. $ret .= " <a href='".$hash->{videolink_ext}."'>Play video from internet (Flash)</a>\n";
  2014. $ret .= " <a href='".$hash->{videolink_int}."'>Play video from local network (Flash)</a>\n";
  2015. }
  2016. else
  2017. {
  2018. $ret .= " no links available";
  2019. }
  2020. return $ret;
  2021. }
  2022. elsif( $cmd eq "updateAll" ) {
  2023. withings_poll($hash,2);
  2024. return undef;
  2025. }
  2026. elsif( $cmd eq "update" ) {
  2027. withings_poll($hash,1);
  2028. return undef;
  2029. }
  2030. elsif( $cmd eq "settings" ) {
  2031. withings_readAuraAlarm($hash);
  2032. return undef;
  2033. }
  2034. } elsif( $hash->{SUBTYPE} eq "ACCOUNT" ) {
  2035. $list = "users:noArg devices:noArg showAccount:noArg";
  2036. if( $cmd eq "users" ) {
  2037. my $users = withings_getUsers($hash);
  2038. my $ret;
  2039. foreach my $user (@{$users}) {
  2040. $ret .= "$user->{id}\t\[$user->{shortname}\]\t$user->{publickey} \t$user->{usertype}/$user->{status}\t$user->{firstname} $user->{lastname}\n";
  2041. }
  2042. $ret = "id\tshort\tpublickey\tusertype/status\tname\n" . $ret if( $ret );;
  2043. $ret = "no users found" if( !$ret );
  2044. return $ret;
  2045. }
  2046. if( $cmd eq "devices" ) {
  2047. my $devices = withings_getDevices($hash);
  2048. my $ret;
  2049. foreach my $device (@{$devices}) {
  2050. my $detail = $device->{deviceproperties};
  2051. $ret .= "$detail->{id}\t$device_types{$detail->{type}}\t$detail->{batterylvl}\t$detail->{sn}\n";
  2052. }
  2053. $ret = "id\ttype\t\tbattery\tSN\n" . $ret if( $ret );;
  2054. $ret = "no devices found" if( !$ret );
  2055. return $ret;
  2056. }
  2057. if( $cmd eq 'showAccount' )
  2058. {
  2059. my $username = $hash->{helper}{username};
  2060. my $password = $hash->{helper}{password};
  2061. return 'no username set' if( !$username );
  2062. return 'no password set' if( !$password );
  2063. $username = withings_decrypt( $username );
  2064. $password = withings_decrypt( $password );
  2065. return "username: $username\npassword: $password";
  2066. }
  2067. }
  2068. return "Unknown argument $cmd, choose one of $list";
  2069. }
  2070. sub withings_Set($$@) {
  2071. my ( $hash, $name, $cmd, @arg ) = @_;
  2072. my $list="";
  2073. if( $hash->{SUBTYPE} eq "DEVICE" and defined($hash->{modelID}) && $hash->{modelID} eq "60" && AttrVal($name,"IP",undef))
  2074. {
  2075. $list = " nap:noArg sleep:noArg alarm:noArg";
  2076. $list .= " stop:noArg snooze:noArg";
  2077. $list .= " nap_volume:slider,0,1,100 nap_brightness:slider,0,1,100";
  2078. $list .= " sleep_volume:slider,0,1,100 sleep_brightness:slider,0,1,100";
  2079. $list .= " clock_state:on,off clock_brightness:slider,0,1,100";
  2080. $list .= " flashMat";
  2081. $list .= " sensors:on,off";
  2082. $list .= " rawCmd";
  2083. if (defined($hash->{helper}{ALARMSCOUNT})&&($hash->{helper}{ALARMSCOUNT}>0))
  2084. {
  2085. for(my $i=1;$i<=$hash->{helper}{ALARMSCOUNT};$i++)
  2086. {
  2087. $list .= " alarm".$i."_time alarm".$i."_volume:slider,0,1,100 alarm".$i."_brightness:slider,0,1,100";
  2088. $list .= " alarm".$i."_state:on,off alarm".$i."_wdays";
  2089. $list .= " alarm".$i."_smartwake:slider,0,1,60";
  2090. }
  2091. }
  2092. if ( lc $cmd eq 'nap' or lc $cmd eq 'sleep' or lc $cmd eq 'alarm' or lc $cmd eq 'stop' or lc $cmd eq 'snooze' )
  2093. {
  2094. return withings_setAuraAlarm($hash,$cmd);
  2095. }
  2096. elsif ( lc $cmd eq 'rawcmd')
  2097. {
  2098. return withings_setAuraDebug($hash,join( "", @arg ));
  2099. }
  2100. #elsif( index( $cmd, "alarm" ) != -1 )
  2101. #{
  2102. # my $alarmno = int( substr( $cmd, 5 ) ) + 0;
  2103. # return( withings_parseAlarm( $hash, $alarmno, @arg ) );
  2104. #}
  2105. elsif ( lc $cmd =~ /^alarm/ or lc $cmd =~ /^nap/ or lc $cmd =~ /^sleep/ or lc $cmd =~ /^clock/ or lc $cmd eq 'smartwake' )
  2106. {
  2107. readingsSingleUpdate( $hash, $cmd, join( ",", @arg ), 1 );
  2108. return withings_setAuraAlarm($hash,$cmd,join( ":", @arg ));
  2109. }
  2110. elsif ( lc $cmd eq "flashmat" )
  2111. {
  2112. return withings_setAuraAlarm($hash,$cmd,join( ":", @arg ));
  2113. }
  2114. elsif ( lc $cmd eq "sensors" )
  2115. {
  2116. return withings_setAuraAlarm($hash,$cmd,join( ":", @arg ));
  2117. }
  2118. return "Unknown argument $cmd, choose one of $list";
  2119. } elsif($hash->{SUBTYPE} eq "ACCOUNT") {
  2120. $list = "autocreate:noArg";
  2121. return withings_autocreate($hash) if($cmd eq "autocreate");
  2122. return "Unknown argument $cmd, choose one of $list";
  2123. } else {
  2124. return "Unknown argument $cmd, choose one of $list";
  2125. }
  2126. }
  2127. sub withings_readAuraAlarm($) {
  2128. my ($hash) = @_;
  2129. my $name = $hash->{NAME};
  2130. Log3 $name, 5, "$name: readauraalarm";
  2131. my $auraip = AttrVal($name,"IP",undef);
  2132. return if(!$auraip);
  2133. my $socket = new IO::Socket::INET (
  2134. PeerHost => $auraip,
  2135. PeerPort => '7685',
  2136. Proto => 'tcp',
  2137. Timeout => 5,
  2138. ) or die "ERROR in Socket Creation : $!\n";
  2139. return if(!$socket);
  2140. $socket->autoflush(1);
  2141. my $data = "000100010100050101010000"; #hello
  2142. $socket->send(pack('H*', $data));
  2143. $socket->flush();
  2144. $socket->recv($data,1024);
  2145. $socket->flush();
  2146. $data="010100050101110000"; #hello2
  2147. $socket->send(pack('H*', $data));
  2148. $socket->flush();
  2149. $socket->recv($data, 1024);
  2150. $socket->flush();
  2151. $data="0101000a01090a0005090a000100"; #ping
  2152. $socket->send(pack('H*', $data));
  2153. $socket->flush();
  2154. $socket->recv($data, 1024);
  2155. $socket->flush();
  2156. $data="010100050101250000"; #new alarmdata
  2157. $socket->send(pack('H*', $data));
  2158. $socket->flush();
  2159. $socket->recv($data, 1024);
  2160. $socket->flush();
  2161. my $datalength = ord(substr($data,2,1))*256 + ord(substr($data,3,1));
  2162. Log3 $name, 5, "$name: alarmdata ($datalength)".unpack('H*', $data);
  2163. my $base = 9;
  2164. readingsBeginUpdate($hash);
  2165. my $alarmcounter = 1;
  2166. my @dataarray = split("05120007",unpack('H*', $data));
  2167. while(defined($dataarray[$alarmcounter]))
  2168. {
  2169. my @alarmparts = split("091600",$dataarray[$alarmcounter]);#seriously, withings?
  2170. my $timedatehex = pack('H*', $alarmparts[0]);
  2171. my $alarmhour = ord(substr($timedatehex,0,1));
  2172. my $alarmminute = ord(substr($timedatehex,1,1));
  2173. my $alarmdays = ord(substr($timedatehex,2,1));
  2174. my $alarmstate = (ord(substr($timedatehex,2,1)) > 128) ? "on" : "off";
  2175. my $alarmperiod = ord(substr($timedatehex,6,1));
  2176. my $alarmvolume = 0;
  2177. my $alarmbrightness = 0;
  2178. my $alarmsong = 0;
  2179. for(my $i=1;$i<=3;$i++) #whoever did this must have been high as fuck!
  2180. {
  2181. my $hexdata = pack('H*', $alarmparts[$i]);
  2182. my $datatype = ord(substr($hexdata,1,1)); #order is not consistent
  2183. my $datalength = ord(substr($hexdata,2,1));
  2184. if($datatype == 1)
  2185. {
  2186. $alarmvolume = ord(substr($hexdata,3,1))-48; #value as ascii characters
  2187. $alarmvolume = $alarmvolume*10 + ord(substr($hexdata,4,1))-48 if($datalength>1);
  2188. $alarmvolume = $alarmvolume*10 + ord(substr($hexdata,5,1))-48 if($datalength>2);
  2189. }
  2190. elsif($datatype == 2)
  2191. {
  2192. $alarmbrightness = ord(substr($hexdata,3,1))-48; #same for other values - wtf?
  2193. $alarmbrightness = $alarmbrightness*10 + ord(substr($hexdata,4,1))-48 if($datalength>1);
  2194. $alarmbrightness = $alarmbrightness*10 + ord(substr($hexdata,5,1))-48 if($datalength>2);
  2195. }
  2196. elsif($datatype == 3)
  2197. {
  2198. $alarmsong = ord(substr($hexdata,3,1))-48;
  2199. }
  2200. else{
  2201. Log3 $name, 2, "$name: unknown alarm data type: $datatype";
  2202. }
  2203. }
  2204. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_time", sprintf( "%02d:%02d:%02d",$alarmhour,$alarmminute,0), 1 );
  2205. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_wdays", withings_int2Weekdays($alarmdays), 1 );
  2206. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_volume", $alarmvolume, 1 );
  2207. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_brightness", $alarmbrightness, 1 );
  2208. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_smartwake", $alarmperiod, 1 );
  2209. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_state", $alarmstate, 1 );
  2210. readingsBulkUpdate( $hash, "alarm".$alarmcounter."_sound", $alarm_sound{$alarmsong}, 1 );
  2211. Log3 $name, 4, "$name: alarm $alarmstate $alarmhour:$alarmminute ($alarmperiod) on ".withings_int2Weekdays($alarmdays)." light:$alarmbrightness vol:$alarmvolume [$base]";
  2212. $hash->{helper}{ALARMSCOUNT} = $alarmcounter;
  2213. $alarmcounter++;
  2214. }
  2215. for(my $i=$alarmcounter;$i<10;$i++)
  2216. {
  2217. fhem( "deletereading $name alarm".$i."_.*" );
  2218. }
  2219. $data="010100050109100000"; #sensordata
  2220. $socket->send(pack('H*', $data));
  2221. $socket->flush();
  2222. $socket->recv($data, 1024);
  2223. $socket->flush();
  2224. my $sensors = (ord(substr($data,19,1))==0)?"on":"off";
  2225. readingsBulkUpdate( $hash, "sensors", $sensors, 1 );
  2226. $data="0101000b0109060006090800020300"; #sleepdata
  2227. $socket->send(pack('H*', $data));
  2228. $socket->flush();
  2229. $socket->recv($data, 1024);
  2230. $socket->flush();
  2231. #Log3 $name, 4, "$name: sleepdata ".unpack('H*', $data);
  2232. my $sleepvolume = ord(substr($data,13,1));
  2233. my $sleepbrightness = ord(substr($data,14,1));
  2234. my $sleepsong = ord(substr($data,16,1));
  2235. readingsBulkUpdate( $hash, "sleep_volume", $sleepvolume, 1 );
  2236. readingsBulkUpdate( $hash, "sleep_brightness", $sleepbrightness, 1 );
  2237. readingsBulkUpdate( $hash, "sleep_sound", $sleep_sound{$sleepsong}, 1 );
  2238. $data="0101000b0109060006090800020200"; #napdata
  2239. $socket->send(pack('H*', $data));
  2240. $socket->flush();
  2241. $socket->recv($data, 1024);
  2242. $socket->flush();
  2243. #Log3 $name, 4, "$name: napdata ".unpack('H*', $data);
  2244. my $napvolume = ord(substr($data,13,1));
  2245. my $napbrightness = ord(substr($data,14,1));
  2246. my $napsong = ord(substr($data,16,1));
  2247. readingsBulkUpdate( $hash, "nap_volume", $napvolume, 1 );
  2248. readingsBulkUpdate( $hash, "nap_brightness", $napbrightness, 1 );
  2249. readingsBulkUpdate( $hash, "nap_sound", $nap_sound{$napsong}, 1 );
  2250. $data="010100050109100000"; #clock
  2251. $socket->send(pack('H*', $data));
  2252. $socket->flush();
  2253. $socket->recv($data, 1024);
  2254. $socket->flush();
  2255. my $clockdisplay = ord(substr($data,13,1));
  2256. my $clockbrightness = ord(substr($data,14,1));
  2257. readingsBulkUpdate( $hash, "clock_state", ($clockdisplay ? "on":"off"), 1 );
  2258. readingsBulkUpdate( $hash, "clock_brightness", $clockbrightness, 1 );
  2259. #Log3 $name, 4, "$name: clock ".unpack('H*', $data);
  2260. $data="010100050109070000"; #state
  2261. $socket->send(pack('H*', $data));
  2262. $socket->flush();
  2263. $socket->recv($data, 1024);
  2264. $socket->flush();
  2265. #Log3 $name, 4, "$name: state ".unpack('H*', $data);
  2266. my $devicestate = ord(substr($data,18,1));
  2267. my $alarmtype = ord(substr($data,13,1));
  2268. if($devicestate eq 0)
  2269. {
  2270. readingsBulkUpdate( $hash, "state", "off", 1 );
  2271. }
  2272. elsif($devicestate eq 2)
  2273. {
  2274. readingsBulkUpdate( $hash, "state", "snoozed", 1 );
  2275. }
  2276. elsif($devicestate eq 1)
  2277. {
  2278. readingsBulkUpdate( $hash, "state", "sleep", 1 ) if($alarmtype eq 1);
  2279. readingsBulkUpdate( $hash, "state", "alarm", 1 ) if($alarmtype eq 2);
  2280. readingsBulkUpdate( $hash, "state", "nap", 1 ) if($alarmtype eq 3);
  2281. }
  2282. readingsEndUpdate($hash,1);
  2283. $socket->close();
  2284. return;
  2285. }
  2286. sub withings_setAuraAlarm($$;$) {
  2287. my ($hash, $setting, $value) = @_;
  2288. my $name = $hash->{NAME};
  2289. Log3 $name, 5, "$name: setaura ".$setting;
  2290. my $auraip = AttrVal($name,"IP",undef);
  2291. return if(!$auraip);
  2292. my $socket = new IO::Socket::INET (
  2293. PeerHost => $auraip,
  2294. PeerPort => '7685',
  2295. Proto => 'tcp',
  2296. Timeout => 5,
  2297. ) or die "ERROR in Socket Creation : $!\n";
  2298. return if(!$socket);
  2299. $socket->autoflush(1);
  2300. my $data = "000100010100050101010000"; #hello
  2301. $socket->send(pack('H*', $data));
  2302. $socket->flush();
  2303. $socket->recv($data,1024);
  2304. $socket->flush();
  2305. $data="010100050101110000"; #hello2
  2306. $socket->send(pack('H*', $data));
  2307. $socket->flush();
  2308. $socket->recv($data,1024);
  2309. $socket->flush();
  2310. $data="0101000a01090a0005090a000100"; #ping
  2311. $socket->send(pack('H*', $data));
  2312. $socket->flush();
  2313. $socket->recv($data,1024);
  2314. $socket->flush();
  2315. $data="010100050109070000"; #getstate
  2316. if($setting eq "nap")
  2317. {
  2318. $data="0101000b0109030006090800020200"; #nap
  2319. }
  2320. elsif($setting eq "sleep")
  2321. {
  2322. $data="0101000b0109030006090800020300"; #sleep
  2323. }
  2324. elsif($setting eq "alarm")
  2325. {
  2326. $data="0101000b0109030006090800020400"; #alarm
  2327. }
  2328. elsif($setting eq "stop")
  2329. {
  2330. $data="010100050109040000"; #stop
  2331. }
  2332. elsif($setting eq "snooze")
  2333. {
  2334. $data="010100050109110000"; #snooze
  2335. }
  2336. elsif($setting =~ /^alarm/)
  2337. {
  2338. my $alarmno = int( substr( $setting,5,1 ) ) + 0;
  2339. my $volume = ReadingsVal( $name, "alarm".$alarmno."_volume", 60);
  2340. my $volumestring = "";
  2341. if($volume > 99)
  2342. {
  2343. $volumestring = "050103" . sprintf("%.2x",substr($volume,0,1)+48) . sprintf("%.2x",substr($volume,1,1)+48) . sprintf("%.2x",substr($volume,2,1)+48);
  2344. }
  2345. elsif($volume > 9)
  2346. {
  2347. $volumestring = "040102" . sprintf("%.2x",substr($volume,0,1)+48) . sprintf("%.2x",substr($volume,1,1)+48);
  2348. }
  2349. else
  2350. {
  2351. $volumestring = "030101" . sprintf("%.2x",$volume+48);
  2352. }
  2353. my $brightness = ReadingsVal( $name, "alarm".$alarmno."_brightness", 60);
  2354. my $brightnessstring = "";
  2355. if($brightness > 99)
  2356. {
  2357. $brightnessstring = "050203" . sprintf("%.2x",substr($brightness,0,1)+48) . sprintf("%.2x",substr($brightness,1,1)+48) . sprintf("%.2x",substr($brightness,2,1)+48);;
  2358. }
  2359. elsif($brightness > 9)
  2360. {
  2361. $brightnessstring = "040202" . sprintf("%.2x",substr($brightness,0,1)+48) . sprintf("%.2x",substr($brightness,1,1)+48);
  2362. }
  2363. else
  2364. {
  2365. $brightnessstring = "030201" . sprintf("%.2x",$brightness+48);
  2366. }
  2367. $data = "05120007";
  2368. my @timestr = split(":",ReadingsVal( $name, "alarm".$alarmno."_time", "07:00" ));
  2369. $data .= sprintf("%.2x%.2x",$timestr[0],$timestr[1]);
  2370. my $alarmint = withings_weekdays2Int(ReadingsVal( $name, "alarm".$alarmno."_wdays", "all"));
  2371. $alarmint += 128 if(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on");
  2372. $data .= sprintf("%.2x",$alarmint);
  2373. $data .= "000000";
  2374. $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_smartwake", 10));
  2375. $data .= "091600";
  2376. $data .= $volumestring;
  2377. $data .= "091600";
  2378. $data .= $brightnessstring;
  2379. $data .= "091600030301";
  2380. my $alarmsong = $alarm_song{ReadingsVal( $name, "alarm".$alarmno."_sound", 1)};
  2381. $alarmsong = 1 if(!defined($alarmsong) || $alarmsong==0);
  2382. $data .= sprintf("%.2x",$alarmsong+48);
  2383. my $datalen = length($data)/2;
  2384. $data = "010100".sprintf("%.2x",$datalen+10)."01012900".sprintf("%.2x",$datalen+5)."01260001".sprintf("%.2x",$alarmno).$data;
  2385. #if($setting =~ /volume/i or $setting =~ /brightness/i or $setting =~ /song/i )
  2386. #{
  2387. # $data = "0101000d010905000809060004";
  2388. # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_volume", 60));
  2389. # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_brightness", 60));
  2390. # $data .= "04";
  2391. # $data .= sprintf("%.2x",$alarm_song{ReadingsVal( $name, "alarm".$alarmno."_sound", 1)});
  2392. #}
  2393. #else
  2394. #{
  2395. # $data = "0101001101011b000c09040008";
  2396. # my @timestr = split(":",ReadingsVal( $name, "alarm".$alarmno."_time", "07:00" ));
  2397. # $data .= sprintf("%.2x%.2x",$timestr[0],$timestr[1]);
  2398. # my $alarmint = withings_weekdays2Int(ReadingsVal( $name, "alarm".$alarmno."_wdays", "all"));
  2399. # $alarmint += 128 if(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on");
  2400. # $data .= sprintf("%.2x",$alarmint);
  2401. # $data .= "6565d2";
  2402. # $data .= sprintf("%.2x",(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on" ? 1 : 0));
  2403. # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_smartwake", 10));
  2404. #}
  2405. Log3 $name, 5, "$name: set alarm ".$data;
  2406. }
  2407. elsif($setting =~ /^nap/)
  2408. {
  2409. $data = "0101000d010905000809060004";
  2410. $data .= sprintf("%.2x",ReadingsVal( $name, "nap_volume", 25));
  2411. $data .= sprintf("%.2x",ReadingsVal( $name, "nap_brightness", 25));
  2412. $data .= "02";
  2413. $data .= sprintf("%.2x",ReadingsVal( $name, "nap_sound", 1)==0?1:ReadingsVal( $name, "nap_sound", 1));
  2414. }
  2415. elsif($setting =~ /^sleep/)
  2416. {
  2417. $data = "0101000d010905000809060004";
  2418. $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_volume", 25));
  2419. $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_brightness", 10));
  2420. $data .= "03";
  2421. $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_sound", 1)==0?1:ReadingsVal( $name, "sleep_sound", 1));
  2422. }
  2423. elsif($setting =~ /^clock/)
  2424. {
  2425. $data = "0101000b01090f0006090d0002";
  2426. $data .= (ReadingsVal( $name, "clock_state", 1) ? "01":"00");
  2427. $data .= sprintf("%.2x",ReadingsVal( $name, "clock_brightness", 40));
  2428. }
  2429. elsif($setting =~ /^flashMat/)
  2430. {
  2431. $data = "0101003201090c002d0413001211";
  2432. $data .= unpack('H*', $value);
  2433. $data .= "080a0013000000000000000000004e200000000000ffff";
  2434. }
  2435. elsif($setting =~ /^sensors/)
  2436. {
  2437. $data = "0101000a01090f0005080b000100";
  2438. $data = "0101000a01090f0005080b000101" if($value eq "off");
  2439. }
  2440. Log3 $name, 3, "$name: writesocket ".$data;
  2441. $socket->send(pack('H*', $data));
  2442. $socket->flush();
  2443. $socket->recv($data, 1024);
  2444. $socket->flush();
  2445. Log3 $name, 4, "$name: readsocket ".unpack('H*', $data);
  2446. $socket->close();
  2447. return;
  2448. }
  2449. sub withings_setAuraDebug($$;$) {
  2450. my ($hash, $value) = @_;
  2451. my $name = $hash->{NAME};
  2452. my $auraip = AttrVal($name,"IP",undef);
  2453. return if(!$auraip);
  2454. my $socket = new IO::Socket::INET (
  2455. PeerHost => $auraip,
  2456. PeerPort => '7685',
  2457. Proto => 'tcp',
  2458. Timeout => 5,
  2459. ) or die "ERROR in Socket Creation : $!\n";
  2460. return if(!$socket);
  2461. $socket->autoflush(1);
  2462. my $data = "000100010100050101010000"; #hello
  2463. $socket->send(pack('H*', $data));
  2464. $socket->flush();
  2465. $socket->recv($data,1024);
  2466. $socket->flush();
  2467. $data="010100050101110000"; #hello2
  2468. $socket->send(pack('H*', $data));
  2469. $socket->flush();
  2470. $socket->recv($data,1024);
  2471. $socket->flush();
  2472. $data="0101000a01090a0005090a000100"; #ping
  2473. $socket->send(pack('H*', $data));
  2474. $socket->flush();
  2475. $socket->recv($data,1024);
  2476. $socket->flush();
  2477. $data=$value; #debug
  2478. Log3 $name, 5, "$name: writesocket ".$data;
  2479. Log3 $name, 5, "$name: writesocket ".pack('H*', $data);
  2480. $socket->send(pack('H*', $data));
  2481. $socket->flush();
  2482. $data="";
  2483. $socket->recv($data, 1024);
  2484. $socket->flush();
  2485. Log3 $name, 5, "$name: readsocket ".$data;
  2486. Log3 $name, 5, "$name: readsocket ".unpack('H*', $data);
  2487. $socket->close();
  2488. return;
  2489. }
  2490. sub withings_Attr($$$) {
  2491. my ($cmd, $name, $attrName, $attrVal) = @_;
  2492. return undef if(!defined($defs{$name}));
  2493. my $orig = $attrVal;
  2494. $attrVal = int($attrVal) if($attrName eq "intervalData" or $attrName eq "intervalAlert" or $attrName eq "intervalProperties" or $attrName eq "intervalDebug");
  2495. $attrVal = 300 if($attrName eq "intervalData" && $attrVal < 300 );
  2496. $attrVal = 300 if($attrName eq "intervalDebug" && $attrVal < 300 );
  2497. $attrVal = 120 if($attrName eq "intervalAlert" && $attrVal < 120 );
  2498. $attrVal = 1800 if($attrName eq "intervalProperties" && $attrVal < 1800 );
  2499. if( $attrName eq "disable" ) {
  2500. my $hash = $defs{$name};
  2501. RemoveInternalTimer($hash);
  2502. if( $cmd eq "set" && $attrVal ne "0" ) {
  2503. } else {
  2504. $attr{$name}{$attrName} = 0;
  2505. withings_poll($hash,0);
  2506. }
  2507. }
  2508. elsif( $attrName eq "nossl" ) {
  2509. my $hash = $defs{$name};
  2510. if( $cmd eq "set" && $attrVal ne "0" ) {
  2511. $hash->{'.https'} = "http";
  2512. } else {
  2513. $hash->{'.https'} = "https";
  2514. }
  2515. }
  2516. if( $cmd eq "set" ) {
  2517. if( $orig ne $attrVal ) {
  2518. $attr{$name}{$attrName} = $attrVal;
  2519. return $attrName ." set to ". $attrVal;
  2520. }
  2521. }
  2522. return;
  2523. }
  2524. ##########################
  2525. sub withings_Dispatch($$$) {
  2526. my ($param, $err, $data) = @_;
  2527. my $hash = $param->{hash};
  2528. my $name = $hash->{NAME};
  2529. Log3 $name, 5, "$name: dispatch ".$param->{type};
  2530. if( $err )
  2531. {
  2532. Log3 $name, 1, "$name: http request failed: type $param->{type} - $err";
  2533. }
  2534. elsif( $data )
  2535. {
  2536. $data =~ s/\n//g;
  2537. if( $data !~ /{.*}/ or $data =~ /</)
  2538. {
  2539. Log3 $name, 1, "$name: invalid json detected: " . $param->{type} . " >>".substr( $data, 0, 64 )."<<" if($data ne "[]");
  2540. return undef;
  2541. }
  2542. my $json = eval { JSON->new->utf8(0)->decode($data) };
  2543. if($@)
  2544. {
  2545. Log3 $name, 2, "$name: json evaluation error on dispatch type ".$param->{type}." ".$@;
  2546. return undef;
  2547. }
  2548. Log3 $name, 1, "$name: Dispatch ".$param->{type}." json error ".$json->{error} if(defined($json->{error}));
  2549. Log3 $name, 5, "$name: json returned: ".Dumper($json);
  2550. if(defined($param->{enddate}))
  2551. {
  2552. $json->{requestedenddate} = $param->{enddate};
  2553. }
  2554. if( $param->{type} eq 'deviceReadingsScale' || $param->{type} eq 'deviceReadingsBedside' || $param->{type} eq 'deviceReadingsHome' ) {
  2555. withings_parseMeasurements($hash, $json);
  2556. } elsif( $param->{type} eq 'userReadingsSleep' || $param->{type} eq 'userReadingsSleepDebug' || $param->{type} eq 'userReadingsActivity' ) {
  2557. withings_parseVasistas($hash, $json, $param->{type});
  2558. } elsif( $param->{type} eq 'deviceReadingsBaby' || $param->{type} eq 'deviceAlertsBaby' ) {
  2559. withings_parseEvents($hash, $json);
  2560. } elsif( $param->{type} eq 'deviceAlertsHome' ) {
  2561. withings_parseTimeline($hash, $json);
  2562. } elsif( $param->{type} eq 'userReadingsCommon' ) {
  2563. withings_parseMeasureGroups($hash, $json);
  2564. } elsif( $param->{type} eq 'userDailyAggregate' ) {
  2565. withings_parseAggregate($hash, $json);
  2566. } elsif( $param->{type} eq 'userDailyActivity' ) {
  2567. withings_parseActivity($hash, $json);
  2568. } elsif( $param->{type} eq 'userDailyWorkouts' ) {
  2569. withings_parseWorkouts($hash, $json);
  2570. } elsif( $param->{type} eq 'deviceProperties' ) {
  2571. withings_parseProperties($hash, $json);
  2572. }
  2573. }
  2574. }
  2575. sub withings_encrypt($)
  2576. {
  2577. my ($decoded) = @_;
  2578. my $key = getUniqueId();
  2579. my $encoded;
  2580. return $decoded if( $decoded =~ /crypt:/ );
  2581. for my $char (split //, $decoded) {
  2582. my $encode = chop($key);
  2583. $encoded .= sprintf("%.2x",ord($char)^ord($encode));
  2584. $key = $encode.$key;
  2585. }
  2586. return 'crypt:'.$encoded;
  2587. }
  2588. sub withings_decrypt($)
  2589. {
  2590. my ($encoded) = @_;
  2591. my $key = getUniqueId();
  2592. my $decoded;
  2593. return $encoded if( $encoded !~ /crypt:/ );
  2594. $encoded = $1 if( $encoded =~ /crypt:(.*)/ );
  2595. for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) {
  2596. my $decode = chop($key);
  2597. $decoded .= chr(ord($char)^ord($decode));
  2598. $key = $decode.$key;
  2599. }
  2600. return $decoded;
  2601. }
  2602. ##########################
  2603. sub withings_DbLog_splitFn($) {
  2604. my ($event) = @_;
  2605. my ($reading, $value, $unit) = "";
  2606. Log3 ("dbsplit", 5, "withings dbsplit event ".$event);
  2607. my @parts = split(/ /,$event,3);
  2608. $reading = $parts[0];
  2609. $reading =~ tr/://d;
  2610. $value = $parts[1];
  2611. if($event =~ m/heartPulse/)
  2612. {
  2613. $reading = 'heartPulse';
  2614. $unit = 'bpm';
  2615. }
  2616. elsif($event =~ m/pulseWave/)
  2617. {
  2618. $reading = 'pulseWave';
  2619. $unit = 'm/s';
  2620. }
  2621. elsif($event =~ m/dailyDescent/)
  2622. {
  2623. $reading = 'dailyDescent';
  2624. $unit = 'm';
  2625. }
  2626. elsif($event =~ m/dailyDistance/)
  2627. {
  2628. $reading = 'dailyDistance';
  2629. $unit = 'm';
  2630. }
  2631. elsif($event =~ m/dailyElevation/)
  2632. {
  2633. $reading = 'dailyElevation';
  2634. $unit = 'm';
  2635. }
  2636. elsif($event =~ m/dailySteps/)
  2637. {
  2638. $reading = 'dailySteps';
  2639. $unit = 'steps';
  2640. }
  2641. elsif($event =~ m/steps/)
  2642. {
  2643. $reading = 'steps';
  2644. $unit = 'steps';
  2645. }
  2646. elsif($event =~ m/temperature/)
  2647. {
  2648. $reading = 'temperature';
  2649. $unit = '˚C';
  2650. }
  2651. elsif($event =~ m/bodyTemperature/)
  2652. {
  2653. $reading = 'bodyTemperature';
  2654. $unit = '˚C';
  2655. }
  2656. elsif($event =~ m/skinTemperature/)
  2657. {
  2658. $reading = 'skinTemperature';
  2659. $unit = '˚C';
  2660. }
  2661. elsif($event =~ m/humidity/)
  2662. {
  2663. $reading = 'humidity';
  2664. $unit = '%';
  2665. }
  2666. elsif($event =~ m/systolicBloodPressure/)
  2667. {
  2668. $reading = 'systolicBloodPressure';
  2669. $unit = 'mmHg';
  2670. }
  2671. elsif($event =~ m/diastolicBloodPressure/)
  2672. {
  2673. $reading = 'diastolicBloodPressure';
  2674. $unit = 'mmHg';
  2675. }
  2676. elsif($event =~ m/spo2/)
  2677. {
  2678. $reading = 'spo2';
  2679. $unit = '%';
  2680. }
  2681. elsif($event =~ m/boneMassWeight/)
  2682. {
  2683. $reading = 'boneMassWeight';
  2684. $unit = 'kg';
  2685. }
  2686. elsif($event =~ m/fatFreeMass/)
  2687. {
  2688. $reading = 'fatFreeMass';
  2689. $unit = 'kg';
  2690. }
  2691. elsif($event =~ m/fatMassWeight/)
  2692. {
  2693. $reading = 'fatMassWeight';
  2694. $unit = 'kg';
  2695. }
  2696. elsif($event =~ m/weight/)
  2697. {
  2698. $reading = 'weight';
  2699. $unit = 'kg';
  2700. }
  2701. elsif($event =~ m/muscleRatio/)
  2702. {
  2703. $reading = 'muscleRatio';
  2704. $unit = '%';
  2705. }
  2706. elsif($event =~ m/boneRatio/)
  2707. {
  2708. $reading = 'boneRatio';
  2709. $unit = '%';
  2710. }
  2711. elsif($event =~ m/fatRatio/)
  2712. {
  2713. $reading = 'fatRatio';
  2714. $unit = '%';
  2715. }
  2716. elsif($event =~ m/hydration/)
  2717. {
  2718. $reading = 'hydration';
  2719. $unit = '%';
  2720. }
  2721. elsif($event =~ m/waterMass/)
  2722. {
  2723. $reading = 'waterMass';
  2724. $unit = 'kg';
  2725. }
  2726. elsif($event =~ m/dailyCaloriesPassive/)
  2727. {
  2728. $reading = 'dailyCaloriesPassive';
  2729. $unit = 'kcal';
  2730. }
  2731. elsif($event =~ m/dailyCaloriesActive/)
  2732. {
  2733. $reading = 'dailyCaloriesActive';
  2734. $unit = 'kcal';
  2735. }
  2736. elsif($event =~ m/calories/)
  2737. {
  2738. $reading = 'calories';
  2739. $unit = 'kcal';
  2740. }
  2741. elsif($event =~ m/co2/)
  2742. {
  2743. $reading = 'co2';
  2744. $unit = 'ppm';
  2745. }
  2746. elsif($event =~ m/voc/)
  2747. {
  2748. $reading = 'voc';
  2749. $unit = 'ppm';
  2750. }
  2751. elsif($event =~ m/light/)
  2752. {
  2753. $reading = 'light';
  2754. $unit = 'lux';
  2755. }
  2756. elsif($event =~ m/batteryPercent/)
  2757. {
  2758. $reading = 'batteryPercent';
  2759. $unit = '%';
  2760. }
  2761. else
  2762. {
  2763. $value = $parts[1];
  2764. $value = $value." ".$parts[2] if(defined($parts[2]));
  2765. }
  2766. #Log3 ("dbsplit", 5, "withings dbsplit output ".$reading." / ".$value." / ".$unit);
  2767. return ($reading, $value, $unit);
  2768. }
  2769. sub withings_int2Weekdays( $ ) {
  2770. my ($wdayint) = @_;
  2771. my $wdayargs = '';
  2772. my $weekdays = '';
  2773. $wdayint -= 128 if($wdayint >= 128);
  2774. if($wdayint >= 64)
  2775. {
  2776. $wdayargs.="Sa";
  2777. $wdayint-=64;
  2778. }
  2779. if($wdayint >= 32)
  2780. {
  2781. $wdayargs.="Fr";
  2782. $wdayint-=32;
  2783. }
  2784. if($wdayint >= 16)
  2785. {
  2786. $wdayargs.="Th";
  2787. $wdayint-=16;
  2788. }
  2789. if($wdayint >= 8)
  2790. {
  2791. $wdayargs.="We";
  2792. $wdayint-=8;
  2793. }
  2794. if($wdayint >= 4)
  2795. {
  2796. $wdayargs.="Tu";
  2797. $wdayint-=4;
  2798. }
  2799. if($wdayint >= 2)
  2800. {
  2801. $wdayargs.="Mo";
  2802. $wdayint-=2;
  2803. }
  2804. if($wdayint >= 1)
  2805. {
  2806. $wdayargs.="Su";
  2807. $wdayint-=1;
  2808. }
  2809. if(index($wdayargs,"Mo") != -1)
  2810. {
  2811. $weekdays.='Mo,';
  2812. }
  2813. if(index($wdayargs,"Tu") != -1)
  2814. {
  2815. $weekdays.='Tu,';
  2816. }
  2817. if(index($wdayargs,"We") != -1)
  2818. {
  2819. $weekdays.='We,';
  2820. }
  2821. if(index($wdayargs,"Th") != -1)
  2822. {
  2823. $weekdays.='Th,';
  2824. }
  2825. if(index($wdayargs,"Fr") != -1)
  2826. {
  2827. $weekdays.='Fr,';
  2828. }
  2829. if(index($wdayargs,"Sa") != -1)
  2830. {
  2831. $weekdays.='Sa,';
  2832. }
  2833. if(index($wdayargs,"Su") != -1)
  2834. {
  2835. $weekdays.='Su,';
  2836. }
  2837. if($weekdays eq "Mo,Tu,We,Th,Fr,Sa,Su,")
  2838. {
  2839. $weekdays="all";
  2840. }
  2841. if($weekdays eq "")
  2842. {
  2843. $weekdays="none";
  2844. }
  2845. $weekdays=~ s/,$//;
  2846. return $weekdays;
  2847. }
  2848. sub withings_weekdays2Int( $ ) {
  2849. my ($wdayargs) = @_;
  2850. my $weekdays = 0;
  2851. if(index($wdayargs,"Mo") != -1 || index($wdayargs,"1") != -1)
  2852. {
  2853. $weekdays+=2;
  2854. }
  2855. if(index($wdayargs,"Tu") != -1 || index($wdayargs,"Di") != -1 || index($wdayargs,"2") != -1)
  2856. {
  2857. $weekdays+=4;
  2858. }
  2859. if(index($wdayargs,"We") != -1 || index($wdayargs,"Mi") != -1 || index($wdayargs,"3") != -1)
  2860. {
  2861. $weekdays+=8;
  2862. }
  2863. if(index($wdayargs,"Th") != -1 || index($wdayargs,"Do") != -1 || index($wdayargs,"4") != -1)
  2864. {
  2865. $weekdays+=16;
  2866. }
  2867. if(index($wdayargs,"Fr") != -1 || index($wdayargs,"5") != -1)
  2868. {
  2869. $weekdays+=32;
  2870. }
  2871. if(index($wdayargs,"Sa") != -1 || index($wdayargs,"6") != -1)
  2872. {
  2873. $weekdays+=64;
  2874. }
  2875. if(index($wdayargs,"Su") != -1 || index($wdayargs,"So") != -1 || index($wdayargs,"0") != -1)
  2876. {
  2877. $weekdays+=1;
  2878. }
  2879. if(index($wdayargs,"all") != -1 || index($wdayargs,"daily") != -1 || index($wdayargs,"7") != -1)
  2880. {
  2881. $weekdays=127;
  2882. }
  2883. if(index($wdayargs,"none") != -1 || index($wdayargs,"once") != -1)
  2884. {
  2885. $weekdays=0;
  2886. }
  2887. return $weekdays;
  2888. }
  2889. 1;
  2890. =pod
  2891. =item device
  2892. =item summary Withings health data for users and devices
  2893. =begin html
  2894. <a name="withings"></a>
  2895. <h3>withings</h3>
  2896. <ul>
  2897. FHEM module for Withings devices.<br><br>
  2898. Notes:
  2899. <ul>
  2900. <li>JSON and Digest::SHA have to be installed on the FHEM host.</li>
  2901. </ul><br>
  2902. <a name="withings_Define"></a>
  2903. <b>Define</b>
  2904. <ul>
  2905. <code>define &lt;name&gt; withings ACCOUNT &lt;login@email&gt; &lt;password&gt;</code><br>
  2906. <code>define &lt;name&gt; withings &lt;device&gt;</code><br>
  2907. <br>
  2908. Defines a withings device.<br><br>
  2909. If a withings device of the account type is created all fhem devices for users and devices are automaticaly created.
  2910. <br>
  2911. Examples:
  2912. <ul>
  2913. <code>define withings withings ACCOUNT abc@test.com myPassword</code><br>
  2914. </ul>
  2915. </ul><br>
  2916. <a name="withings_Readings"></a>
  2917. <b>Readings</b>
  2918. <ul>
  2919. <li>height</li>
  2920. <li>weight</li>
  2921. <li>fatFreeMass</li>
  2922. <li>muscleRatio</li>
  2923. <li>fatMassWeight</li>
  2924. <li>fatRatio</li>
  2925. <li>boneMassWeight</li>
  2926. <li>boneRatio</li>
  2927. <li>hydration</li>
  2928. <li>diastolicBloodPressure</li>
  2929. <li>systolicBloodPressure</li>
  2930. <li>heartPulse</li>
  2931. <li>pulseWave</li>
  2932. <li>spo2</li>
  2933. <li>bodyTemperature</li>
  2934. <li>skinTemperature</li>
  2935. <li>temperature</li>
  2936. <li>dailySteps</li>
  2937. <li>dailyDistance</li>
  2938. <li>dailyElevation</li>
  2939. <li>dailyDescent</li>
  2940. <li>dailyDurationLight</li>
  2941. <li>dailyDurationModerate</li>
  2942. <li>dailyDurationIntense</li>
  2943. <li>dailyCaloriesActive</li>
  2944. <li>dailyCaloriesPassive</li>
  2945. <li>sleepDurationAwake</li>
  2946. <li>sleepDurationLight</li>
  2947. <li>sleepDurationDeep</li>
  2948. <li>sleepDurationREM</li>
  2949. <li>wakeupCount</li>
  2950. <li>co2</li>
  2951. <li>temperature</li>
  2952. <li>light</li>
  2953. <li>noise</li>
  2954. <li>voc</li>
  2955. <li>batteryState</li>
  2956. <li>batteryPercent</li>
  2957. </ul><br>
  2958. <a name="withings_Get"></a>
  2959. <b>Get</b>
  2960. <ul>
  2961. <li>update<br>
  2962. trigger an update</li>
  2963. </ul><br>
  2964. <a name="withings_Attr"></a>
  2965. <b>Attributes</b>
  2966. <ul>
  2967. <li>interval<br>
  2968. the interval in seconds used to check for new values.</li>
  2969. <li>disable<br>
  2970. 1 -> stop polling</li>
  2971. </ul>
  2972. </ul>
  2973. =end html
  2974. =cut