32_withings.pm 125 KB

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