32_withings.pm 124 KB

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