32_withings.pm 122 KB

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