From fa5011195b37d47eb27d80b214934b5450cf3241 Mon Sep 17 00:00:00 2001 From: moises <> Date: Sun, 19 Feb 2017 23:24:46 +0000 Subject: [PATCH] 38_netatmo.pm: bugfixing / 32_withings.pm: new module git-svn-id: https://svn.fhem.de/fhem/trunk@13458 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 5 +- fhem/FHEM/32_withings.pm | 3191 ++++++++++++++++++++++++++++++++++---- fhem/FHEM/38_netatmo.pm | 324 ++-- fhem/HISTORY | 5 +- 4 files changed, 3108 insertions(+), 417 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 9d91caa2b..2f28b0956 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,8 +1,9 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - bugfix: 38_netatmo.pm: fixed login crash, updated webhooks + - update: 32_withings.pm: replaced with new module - feature: 5.8 released - -- 2017-02-19 (5.8) + - 2017-02-19 (5.8) - feature: 88_HMCCU: advanced reading filters - feature: 74_AMAD: add closeCall command - bugfix: 98_DOIFtools: some more webname issues diff --git a/fhem/FHEM/32_withings.pm b/fhem/FHEM/32_withings.pm index 7ea866843..b6557708f 100644 --- a/fhem/FHEM/32_withings.pm +++ b/fhem/FHEM/32_withings.pm @@ -1,66 +1,310 @@ - +############################################################################## # $Id$ +# +# 32_withings.pm +# +# 2017 Markus M. +# Based on original code by justme1968 +# +# https://forum.fhem.de/index.php/topic,64944.0.html +# +# +############################################################################## +# Release 01 / 2017-02-19 package main; use strict; use warnings; -use Encode qw(encode); -use JSON; -use LWP::Simple; -use HTTP::Request; -use HTTP::Cookies; +use HttpUtils; -use Digest::MD5 qw(md5 md5_hex md5_base64); +use JSON; use POSIX qw( strftime ); +use Time::Local qw(timelocal); +use Digest::SHA qw(hmac_sha1_base64); + +#use Encode qw(encode); +#use LWP::Simple; +#use HTTP::Request; +#use HTTP::Cookies; +#use URI::Escape qw(uri_escape); + +use Data::Dumper; + +#use Digest::MD5 qw(md5 md5_hex md5_base64); + + + my %device_types = ( 0 => "User related", - 1 => "Body scale", - 4 => "Blood pressure monitor", - 16 => "Withings Pulse", ); + 1 => "Body Scale", + 2 => "Camera", + 4 => "Blood Pressure Monitor", + 16 => "Activity Tracker", + 32 => "Sleep Monitor", + 64 => "Thermometer", ); -my %device_models = ( 1 => { 1 => "Smart scale", 4 => "Body analyzer", }, ); +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", }, + 2 => { 21 => "Smart Baby Monitor", 22 => "Withings Home", }, + 4 => { 41 => "iOS Blood Pressure Monitor", 42 => "Wireless Blood Pressure Monitor", }, + 16 => { 51 => "Pulse Ox", 52 => "Activite", 53 => "Activite v2", 54 => "Withings Go", 55 => "Withings Steel HR", }, + 32 => { 60 => "Withings Aura", 61 => "Aura Sleep Sensor", }, + 64 => { 70 => "Withings Thermo", }, ); + + #Firmware files: cdnfw_withings_net + #Smart Body Analyzer: /wbs02/wbs02_1521.bin + #Blood Pressure Monitor: /wpm02/wpm02_251.bin + #Pulse: /wam01/wam01_1761.bin + #Aura: /wsd01/wsd01_607.bin + #Aura Mat: /wsm01/wsm01_711.bin + #Home: /wbp02/wbp02_168.bin + #Activite: /hwa01/hwa01_1070.bin my %measure_types = ( 1 => { name => "Weight (kg)", reading => "weight", }, 4 => { name => "Height (meter)", reading => "height", }, - 5 => { name => "Fat Free Mass (kg)", reading => "fatFreeMass", }, - 6 => { name => "Fat Ratio (%)", reading => "fatRatio", }, - 8 => { name => "Fat Mass Weight (kg)", reading => "fatMassWeight", }, + 5 => { name => "Lean Mass (kg)", reading => "fatFreeMass", }, + 6 => { name => "Fat Mass (%)", reading => "fatRatio", }, + 7 => { name => "Lean Mass (%)", reading => "fatFreeRatio", }, + 8 => { name => "Fat Mass (kg)", reading => "fatMassWeight", }, 9 => { name => "Diastolic Blood Pressure (mmHg)", reading => "diastolicBloodPressure", }, 10 => { name => "Systolic Blood Pressure (mmHg)", reading => "systolicBloodPressure", }, - 11 => { name => "Heart Pulse (bpm)", reading => "heartPulse", }, + 11 => { name => "Heart Rate (bpm)", reading => "heartPulse", }, 12 => { name => "Temperature (°C)", reading => "temperature", }, + 13 => { name => "Humidity (%)", reading => "humidity", }, + 14 => { name => "unknown 14", reading => "unknown14", }, #device? event home - peak sound level? + 15 => { name => "Noise (dB)", reading => "noise", }, + 18 => { name => "Weight Objective Speed", reading => "weightObjectiveSpeed", }, + 19 => { name => "Breastfeeding (sec)", reading => "breast", }, #baby + 20 => { name => "Bottle (ml)", reading => "bottle", }, #baby + 22 => { name => "BMI", reading => "bmi", }, #user? goals 35 => { name => "CO2 (ppm)", reading => "co2", }, - 54 => { name => "SPo2 (%)", reading => "spo2", }, ); + 36 => { name => "Steps", reading => "steps", dailyreading => "dailySteps", }, #aggregate + 37 => { name => "Elevation (m)", reading => "elevation", dailyreading => "dailyElevation", }, #aggregate + 38 => { name => "Active Calories (kcal)", reading => "calories", dailyreading => "dailyCalories", }, #aggregate + 39 => { name => "Intensity", reading => "intensity", }, #intraday only + 40 => { name => "Distance (m)", reading => "distance", dailyreading => "dailyDistance", }, #aggregate #measure + 41 => { name => "Descent (m)", reading => "descent", dailyreading => "dailyDescent", }, #descent #aggregate #measure ??sleepreading! + 42 => { name => "Activity Type", reading => "activityType", }, #intraday only 1:walk 2:run + 43 => { name => "Duration (s)", reading => "duration", }, #intraday only + 44 => { name => "Sleep State", reading => "sleepstate", }, #intraday #aura mat + 47 => { name => "MyFitnessPal Calories (kcal)", reading => "caloriesMFP", }, + 48 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, #day summary + 49 => { name => "Idle Calories (kcal)", reading => "caloriesPassive", dailyreading => "dailyCaloriesPassive", }, #aggregate + 50 => { name => "unknown 50", reading => "unknown50", dailyreading => "dailyUnknown50", }, #day summary pulse 60k-80k #aggregate + 51 => { name => "Light Activity (s)", reading => "durationLight", dailyreading => "dailyDurationLight", }, #aggregate + 52 => { name => "Moderate Activity (s)", reading => "durationModerate", dailyreading => "dailyDurationModerate", }, #aggregate + 53 => { name => "Intense Activity (s)", reading => "durationIntense", dailyreading => "dailyDurationIntense", }, #aggregate + 54 => { name => "SpO2 (%)", reading => "spo2", }, + 56 => { name => "Ambient light (lux)", reading => "light", }, # aura device + 57 => { name => "Respiratory rate", reading => "breathing", }, # aura mat #measure vasistas + 58 => { name => "Air Quality (ppm)", reading => "voc", }, # Home Air Quality + 59 => { name => "unknown 59", reading => "unknown59", }, # + 60 => { name => "unknown 60", reading => "unknown60", }, # aura mat #measure vasistas 20-200 peak 800 + 61 => { name => "unknown 61", reading => "unknown61", }, # aura mat #measure vasistas 10-60 peak 600 + 62 => { name => "unknown 62", reading => "unknown62", }, # aura mat #measure vasistas 20-100 + 63 => { name => "unknown 63", reading => "unknown63", }, # aura mat #measure vasistas 0-100 + 64 => { name => "unknown 64", reading => "unknown64", }, # aura mat #measure vasistas 800-1300 + 65 => { name => "unknown 65", reading => "unknown65", }, # aura mat #measure vasistas 3000-4500 peak 5000 + 66 => { name => "unknown 66", reading => "unknown66", }, # aura mat #measure vasistas 4000-7000 + 67 => { name => "unknown 67", reading => "unknown67", }, # aura mat #measure vasistas 0-500 peak 1500 + 68 => { name => "unknown 68", reading => "unknown68", }, # aura mat #measure vasistas 0-1500 + 69 => { name => "unknown 69", reading => "unknown69", }, # aura mat #measure vasistas 0-6000 peak 10000 + 70 => { name => "unknown 70", reading => "unknown70", }, #? + 71 => { name => "Body Temperature (°C)", reading => "bodyTemperature", }, #thermo + 73 => { name => "Skin Temperature (°C)", reading => "skinTemperature", }, #thermo + 76 => { name => "Muscle Mass (kg)", reading => "muscleMass", }, # cardio scale + 77 => { name => "Water Mass (kg)", reading => "waterMass", }, # cardio scale + 78 => { name => "unknown 78", reading => "unknown78", }, # cardio scale + 87 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, # measures list sleepreading! + 88 => { name => "Bone Mass (kg)", reading => "boneMassWeight", }, + 89 => { name => "unknown 89", reading => "unknown89", }, + 90 => { name => "unknown 90", reading => "unknown90", }, + 91 => { name => "Pulse Wave Velocity (m/s)", reading => "pulseWave", }, # new weight + 93 => { name => "Muscle Mass (%)", reading => "muscleRatio", }, # cardio scale + 94 => { name => "Bone Mass (%)", reading => "boneRatio", }, # cardio scale + 95 => { name => "Hydration (%)", reading => "hydration", }, # body water + #-10 => { name => "Speed", reading => "speed", }, + #-11 => { name => "Pace", reading => "pace", }, + #-12 => { name => "Altitude", reading => "altitude", }, + ); -sub -withings_Initialize($) -{ +my %activity_types = ( 0 => "None", + 1 => "Walking", + 2 => "Running", + 3 => "Hiking", + 4 => "Skating", + 5 => "BMX", + 6 => "Cycling", + 7 => "Swimming", + 8 => "Surfing", + 9 => "Kitesurfing", + 10 => "Windsurfing", + 11 => "Bodyboard", + 12 => "Tennis", + 13 => "Ping Pong", + 14 => "Squash", + 15 => "Badminton", + 16 => "Weights", + 17 => "Calisthenics", + 18 => "Elliptical", + 19 => "Pilates", + 20 => "Basketball", + 21 => "Soccer", + 22 => "Football", + 23 => "Rugby", + 24 => "Vollyball", + 25 => "Water Polo", + 26 => "Horse Riding", + 27 => "Golf", + 28 => "Yoga", + 29 => "Dancing", + 30 => "Boxing", + 31 => "Fencing", + 32 => "Wrestling", + 33 => "Martial Arts", + 34 => "Skiing", + 35 => "Snowboarding", + 36 => "Other", + 37 => "Sleep", + 127 => "Sleep Debug", + 128 => "No Activity", + 187 => "Rowing", + 188 => "Zumba", + 190 => "Base", + 191 => "Baseball", + 192 => "Handball", + 193 => "Hockey", + 194 => "Ice Hockey", + 195 => "Climbing", + 196 => "Ice Skating", + 271 => "Multi Sports", + 272 => "Multi Sport", ); + +my %sleep_state = ( 0 => "awake", + 1 => "light sleep", + 2 => "deep sleep", + 3 => "REM sleep", ); + +my %weight_units = ( 1 => { name => "kg (metric)", unit => "kg", }, + 2 => { name => "lb (US imperial)", unit => "lb", }, + 5 => { name => "stlb (UK imperial)", unit => "st", }, + 14 => { name => "stlb (UK imperial)", unit => "st", }, ); + +my %distance_units = ( 6 => { name => "km", unit => "km", }, + 7 => { name => "miles", unit => "mi", }, ); + +my %temperature_units = ( 11 => { name => "Celsius", unit => "˚C", }, + 13 => { name => "Fahrenheit", unit => "˚F", }, ); + +my %height_units = ( 6 => { name => "cm", unit => "cm", }, + 7 => { name => "ft", unit => "ft", }, ); + +my %aggregate_range = ( 1 => "day", + 2 => "week", + 3 => "month", + 4 => "year", + 5 => "alltime", ); + +my %event_types = ( 10 => { name => "Noise", reading => "alertNoise", threshold => "levelNoise", duration => "durationNoise", unit => 0, }, + 11 => { name => "Motion", reading => "alertMotion", threshold => "levelMotion", duration => "durationMotion", unit => -2, }, + 12 => { name => "Low Temperature", reading => "alertTemperatureLow", threshold => "levelTemperatureLow", duration => "dummy", unit => -2, }, + 13 => { name => "High Temperature", reading => "alertTemperatureHigh", threshold => "levelTemperatureHigh", duration => "dummy", unit => -2, }, + 14 => { name => "Low Humidity", reading => "alertHumidityLow", threshold => "levelHumidityLow", duration => "dummy", unit => -2, }, + 15 => { name => "High Humidity", reading => "alertHumidityHigh", threshold => "levelHumidityHigh", duration => "dummy", unit => -2, }, + 20 => { name => "Disconnection", reading => "alertDisconnection", threshold => "levelDisconnected", duration => "dummy", unit => 0, }, + ); + +# +my %timeline_classes = ( 'noise_detected' => { name => "Noise", reading => "alertNoise", unit => 0, }, + 'movement_detected' => { name => "Motion", reading => "alertMotion", unit => 0, }, + 'alert_environment' => { name => "Air Quality Alert", reading => "alertEnvironment", unit => 0, }, + 'period_activity' => { name => "Activity Period", reading => "periodActivity", unit => 0, }, + 'period_activity_start' => { name => "Activity Period Start", reading => "periodActivityStart", unit => 0, }, + 'period_activity_cancel' => { name => "Activity Period Cancel", reading => "periodActivityCancel", unit => 0, }, + 'period_offline' => { name => "Offline Period", reading => "periodDisconnection", unit => 0, }, + 'offline' => { name => "Disconnection", reading => "alertDisconnection", unit => 0, }, + 'online' => { name => "Connection", reading => "alertConnection", unit => 0, }, + 'deleted' => { name => "Deleted", reading => "alertDeleted", unit => 0, }, + 'snapshot' => { name => "Snapshot", reading => "alertSnapshot", unit => 0, }, + ); + +my %sleep_readings = ( 'lightsleepduration' => { name => "Light Sleep", reading => "sleepDurationLight", unit => 0, }, + 'deepsleepduration' => { name => "Deep Sleep", reading => "sleepDurationDeep", unit => 0, }, + 'remsleepduration' => { name => "REM Sleep", reading => "sleepDurationREM", unit => 0, }, + 'wakeupduration' => { name => "Awake In Bed", reading => "sleepDurationAwake", unit => 0, }, + 'wakeupcount' => { name => "Wakeup Count", reading => "wakeupCount", unit => 0, }, + 'durationtosleep' => { name => "Duration To Sleep", reading => "durationToSleep", unit => 0, }, + 'durationtowakeup' => { name => "Duration To Wake Up", reading => "durationToWakeUp", unit => 0, }, + 'sleepscore' => { name => "Sleep Score", reading => "sleepScore", unit => 0, }, + 'wsdid' => { name => "wsdid", reading => "wsdid", unit => 0, }, + ); + +my %alarm_sound = ( 0 => "Unknown", + 1 => "Cloud Flakes", + 2 => "Desert Wave", + 3 => "Moss Forest", + 4 => "Morning Smile", + 5 => "Spotify", + 6 => "Internet radio", ); + +my %alarm_song = ( 'Unknown' => 0, + 'Cloud Flakes' => 1, + 'Desert Wave' => 2, + 'Moss Forest' => 3, + 'Morning Smile' => 4, + 'Spotify' => 5, + 'Internet radio' => 6, ); + +my %nap_sound = ( 0 => "Unknown", + 1 => "Celestial Piano (20 min)", + 2 => "Cotton Cloud (10 min)", + 3 => "Deep Smile (10 min)", + 4 => "Sacred Forest (20 min)", ); + + +my %sleep_sound = ( 0 => "Unknown", + 1 => "Moonlight Waves", + 2 => "Siren's Whisper", + 3 => "Celestial Piano", + 4 => "Cloud Flakes", + 5 => "Spotify", + 6 => "Internet radio", ); + + +sub withings_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "withings_Define"; + $hash->{SetFn} = "withings_Set"; + $hash->{GetFn} = "withings_Get"; $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "withings_Notify"; $hash->{UndefFn} = "withings_Undefine"; - #$hash->{SetFn} = "withings_Set"; - $hash->{GetFn} = "withings_Get"; + $hash->{DbLog_splitFn} = "withings_DbLog_splitFn"; $hash->{AttrFn} = "withings_Attr"; $hash->{AttrList} = "IODev ". - "disable:1 ". - "interval ". - "logfile ". - "nossl:1 "; + "disable:0,1 ". + "intervalAlert ". + "intervalData ". + "intervalDebug ". + "intervalProperties ". + "intervalDaily ". + "nossl:1 ". + "IP ". + "videoLinkEvents:1 "; + $hash->{AttrList} .= $readingFnAttributes; + +Log3 "withings", 5, "withings: initialize"; } ##################################### -sub -withings_Define($$) -{ +sub withings_Define($$) { my ($hash, $def) = @_; + Log3 "withings", 5, "withings: define ".$def; my @a = split("[ \t][ \t]*", $def); @@ -73,14 +317,23 @@ withings_Define($$) $hash->{Device} = $device; - $hash->{INTERVAL} = 3600; - my $d = $modules{$hash->{TYPE}}{defptr}{"D$device"}; return "device $device already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); $modules{$hash->{TYPE}}{defptr}{"D$device"} = $hash; - } elsif( @a == 4 && $a[2] =~ m/\d+/ && $a[3] =~ m/[\da-f]+/ ) { + } elsif( @a == 5 && $a[2] =~ m/^\D+$/ && $a[3] =~ m/^\d+$/ ) { + $subtype = "DUMMY"; + my $device = $a[2]; + my $user = $a[3]; + $hash->{Device} = $device; + $hash->{typeID} = '16'; + $hash->{modelID} = '0'; + $hash->{User} = $user; + + CommandAttr(undef,"$name IODev $a[4]"); + + } elsif( @a == 4 && $a[2] =~ m/^\d+$/ && $a[3] =~ m/^[\w]+$/i ) { $subtype = "USER"; my $user = $a[2]; @@ -89,8 +342,6 @@ withings_Define($$) $hash->{User} = $user; $hash->{Key} = $key; - $hash->{INTERVAL} = 3600; - my $d = $modules{$hash->{TYPE}}{defptr}{"U$user"}; return "device $user already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); @@ -106,6 +357,8 @@ withings_Define($$) $hash->{Login} = $login; $hash->{Password} = $password; + $hash->{appliver} = undef; + $hash->{csrf_token} = undef; } else { return "Usage: define withings device\ define withings userid publickey\ @@ -115,36 +368,91 @@ withings_Define($$) $hash->{NAME} = $name; $hash->{SUBTYPE} = $subtype; + + #CommandAttr(undef,"$name DbLogExclude .*"); + + + my $resolve = inet_aton("healthmate.withings.com"); + if(!defined($resolve)) + { + $hash->{STATE} = "DNS error"; + InternalTimer( gettimeofday() + 900, "withings_InitWait", $hash, 0); + return undef; + } + $hash->{STATE} = "Initialized"; if( $init_done ) { withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" ); withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" ); + InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0) if( $hash->{SUBTYPE} eq "DUMMY" ); } + else + { + InternalTimer(gettimeofday()+15, "withings_InitWait", $hash, 0); + } + return undef; } -sub -withings_Notify($$) -{ + +sub withings_InitWait($) { + my ($hash) = @_; + Log3 "withings", 5, "withings: initwait ".$init_done; + + RemoveInternalTimer($hash); + + my $resolve = inet_aton("healthmate.withings.com"); + if(!defined($resolve)) + { + $hash->{STATE} = "DNS error"; + InternalTimer( gettimeofday() + 1800, "withings_InitWait", $hash, 0); + return undef; + } + + if( $init_done ) { + withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" ); + withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); + withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" ); + InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0) if( $hash->{SUBTYPE} eq "DUMMY" ); + } + else + { + InternalTimer(gettimeofday()+30, "withings_InitWait", $hash, 0); + } + + return undef; + +} + +sub withings_Notify($$) { my ($hash,$dev) = @_; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); + Log3 "withings", 5, "withings: notify"; + + my $resolve = inet_aton("healthmate.withings.com"); + if(!defined($resolve)) + { + $hash->{STATE} = "DNS error"; + InternalTimer( gettimeofday() + 3600, "withings_InitWait", $hash, 0); + return undef; + } + withings_initUser($hash) if( $hash->{SUBTYPE} eq "USER" ); withings_connect($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); withings_initDevice($hash) if( $hash->{SUBTYPE} eq "DEVICE" ); - - return undef; } -sub -withings_Undefine($$) -{ + +sub withings_Undefine($$) { my ($hash, $arg) = @_; + Log3 "withings", 5, "withings: undefine"; + RemoveInternalTimer($hash); delete( $modules{$hash->{TYPE}}{defptr}{"U$hash->{User}"} ) if( $hash->{SUBTYPE} eq "USER" ); delete( $modules{$hash->{TYPE}}{defptr}{"D$hash->{Device}"} ) if( $hash->{SUBTYPE} eq "DEVICE" ); @@ -152,82 +460,196 @@ withings_Undefine($$) return undef; } -sub -withings_Set($$@) -{ - my ($hash, $name, $cmd) = @_; - my $list = ""; - return "Unknown argument $cmd, choose one of $list"; -} - -sub -withings_getToken($) -{ +sub withings_getToken($) { my ($hash) = @_; + Log3 "withings", 5, "withings: gettoken"; - my $URL = 'http://auth.withings.com/index/service/once?action=get'; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + + my $resolve = inet_aton("auth.withings.com"); + if(!defined($resolve)) + { + Log3 "withings", 1, "withings: DNS error on getToken"; + return undef; + } + + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://auth.withings.com/index/service/once?action=get", + timeout => 10, + noshutdown => 1, + data => {action => 'get'}, + }); + + #my $URL = 'http://auth.withings.com/index/service/once?action=get'; + #my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); + #my $header = HTTP::Request->new(GET => $URL); + #my $request = HTTP::Request->new('GET', $URL, $header); + #my $response = $agent->request($request); + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); - + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); my $once = $json->{body}{once}; - - $hash->{Token} = $once; - + $hash->{Once} = $once; my $hashstring = $hash->{Login}.':'.md5_hex($hash->{Password}).':'.$once; - $hash->{Hash} = md5_hex($hashstring); } -sub -withings_getSessionKey($) -{ - my ($hash) = @_; - - return if( $hash->{SessionTimestamp} && gettimeofday() - $hash->{SessionTimestamp} < 300 ); - - withings_getToken($hash); - - my $URL='http://auth.withings.com/en/'; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $response = $agent->post($URL, [email => $hash->{Login}, password => $hash->{Password}, rememberme => 'on', hash => $hash->{Hash}, once => $hash->{Token}, passClear => $hash->{Password}]); - - my $authcookies=$response->header('Set-Cookie'); - $authcookies =~ /session_key=([\s\S]+?);/; - - $hash->{SessionKey} = $1; - $hash->{SessionTimestamp} = (gettimeofday())[0]; - - $hash->{STATE} = "Connected" if( $hash->{SessionKey} ); - $hash->{STATE} = "Error" if( !$hash->{SessionKey} ); - - if( !$hash->{AccountID} || length($hash->{AccountID} < 2 ) ) { - my $URL = 'http://healthmate.withings.com/index/service/account?applitype=20&action=get&sessionid='.$hash->{SessionKey}; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); - - my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); - - foreach my $account (@{$json->{body}{account}}) { - next if( !defined($account->{id}) ); - $hash->{AccountID} = $account->{id} if($account->{email} eq $hash->{Login}); - } - } -} -sub -withings_connect($) -{ +sub withings_getSessionKey($) { my ($hash) = @_; my $name = $hash->{NAME}; + return if( $hash->{SUBTYPE} ne "ACCOUNT" ); + + my $resolve = inet_aton("healthmate.withings.com"); + if(!defined($resolve)) + { + $hash->{SessionTimestamp} = 0; + Log3 "withings", 1, "$name: DNS error on getSessionData"; + return undef; + } + + return if( $hash->{SessionKey} && $hash->{SessionTimestamp} && gettimeofday() - $hash->{SessionTimestamp} < (60*60*24*7-3600) ); + + $hash->{'.https'} = "https" if(!defined($hash->{'.https'})); + + my $data1; + if( !defined($hash->{SessionTimestamp}) || gettimeofday() - $hash->{SessionTimestamp} > (30*60) )#!defined($hash->{appliver}) || !defined($hash->{csrf_token})) + { + my($err0,$data0) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/", + timeout => 10, + noshutdown => 1, + }); + if($err0 || !defined($data0)) + { + Log3 "withings", 1, "$name: appliver call failed! ".$err0; + return undef; + } + $data1 = $data0; + $data0 =~ /appliver=([^.*]+)\&/; + $hash->{appliver} = $1; + if(!defined($hash->{appliver})) { + Log3 "withings", 1, "$name: APPLIVER ERROR "; + $hash->{STATE} = "APPLIVER error"; + return undef; + } + Log3 "withings", 4, "$name: appliver ".$hash->{appliver}; + #} + + + #if( !defined($hash->{csrf_token}) ) + #{ + $data1 =~ /csrf_token" value="(.*)"/; + $hash->{csrf_token} = $1; + + if(!defined($hash->{csrf_token})) { + Log3 "withings", 1, "$name: CSRF ERROR "; + $hash->{STATE} = "CSRF error"; + return undef; + } + Log3 "withings", 4, "$name: csrf_token ".$hash->{csrf_token}; + } + + #my $ua = LWP::UserAgent->new; + #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://account.withings.com/connectionuser/account_login?appname=my2&appliver='.$hash->{appliver}.'&r=https%3A%2F%2Fhealthmate.withings.com%2F',[email => $hash->{Login}, password => $hash->{Password}, is_admin => '',]); + #my $get_data = 'use_authy=&is_admin=&email='.uri_escape($hash->{Login}).'&password='.uri_escape($hash->{Password}); + #$request->content($get_data); + #my $response = $ua->request($request); + + $resolve = inet_aton("account.withings.com"); + if(!defined($resolve)) + { + Log3 "withings", 1, "$name: DNS error on getSessionKey."; + return undef; + } + + my $datahash = { + url => $hash->{'.https'}."://account.withings.com/connectionuser/account_login?appname=my2&appliver=".$hash->{appliver}."&r=https%3A%2F%2Fhealthmate.withings.com%2F", + timeout => 10, + noshutdown => 1, + ignoreredirects => 1, + data => { email=> $hash->{Login}, password => $hash->{Password}, is_admin => '', csrf_token => $hash->{csrf_token} }, + }; + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 "withings", 1, "$name: LOGIN ERROR "; + $hash->{STATE} = "Login error"; + return undef; + } + else + { + if ($datahash->{httpheader} =~ /session_key=(.*?);/) + { + $hash->{SessionKey} = $1; + $hash->{SessionTimestamp} = (gettimeofday())[0] if( $hash->{SessionKey} ); + $hash->{STATE} = "Connected" if( $hash->{SessionKey} ); + $hash->{STATE} = "Session error" if( !$hash->{SessionKey} ); + Log3 "withings", 4, "$name: sessionkey ".$hash->{SessionKey}; + } + else + { + $hash->{STATE} = "Cookie error"; + Log3 "withings", 1, "$name: COOKIE ERROR "; + $hash->{appliver} = undef; + $hash->{csrf_token} = undef; + return undef; + } + } + + if( !$hash->{AccountID} || length($hash->{AccountID} < 2 ) ) { + + ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/account", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{SessionKey}, appname => 'my2', appliver=> $hash->{appliver}, apppfm => 'web', action => 'get'}, + }); + return undef if(!defined($data)); + + if( $data =~ m/^{.*}$/ ) + { + my $json = (); + $json = JSON->new->utf8(0)->decode($data); + + foreach my $account (@{$json->{body}{account}}) { + next if( !defined($account->{id}) ); + if($account->{email} eq $hash->{Login}) + { + $hash->{AccountID} = $account->{id}; + } + else + { + Log3 "withings", 4, "$name: account email: ".$account->{email}; + } + } + Log3 "withings", 4, "$name: accountid ".$hash->{AccountID}; + } + else + { + $hash->{STATE} = "Account error"; + Log3 "withings", 1, "$name: ACCOUNT ERROR "; + return undef; + } + } + +} + + +sub withings_connect($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: connect"; + + $hash->{'.https'} = "https"; + $hash->{'.https'} = "http" if( AttrVal($name, "nossl", 0) ); + + #my $cmdret= CommandAttr(undef,"$name room WithingsTest"); + #$cmdret= CommandAttr(undef,"$name verbose 5"); + withings_getSessionKey( $hash ); foreach my $d (keys %defs) { @@ -240,49 +662,61 @@ withings_connect($) my $users = withings_getUsers($hash); foreach my $user (@{$users}) { if( defined($modules{$hash->{TYPE}}{defptr}{"U$user->{id}"}) ) { - Log3 $name, 4, "$name: user '$user->{id}' already defined"; + Log3 $name, 2, "$name: user '$user->{id}' already defined"; next; } + next if($user->{firstname} eq "Repository-User"); my $id = $user->{id}; my $devname = "withings_U". $id; my $define= "$devname withings $id $user->{publickey}"; - Log3 $name, 3, "$name: create new device '$devname' for user '$id'"; + Log3 $name, 2, "$name: create new device '$devname' for user '$id'"; my $cmdret= CommandDefine(undef,$define); if($cmdret) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret"; } else { $cmdret= CommandAttr(undef,"$devname alias ".$user->{shortname}); - $cmdret= CommandAttr(undef,"$devname room withings"); + #$cmdret= CommandAttr(undef,"$devname room WithingsTest"); $cmdret= CommandAttr(undef,"$devname IODev $name"); + #$cmdret= CommandAttr(undef,"$devname disable 1"); + #$cmdret= CommandAttr(undef,"$devname verbose 5"); $autocreated++; } } + my $devices = withings_getDevices($hash); foreach my $device (@{$devices}) { if( defined($modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"}) ) { - Log3 $name, 4, "$name: device '$device->{deviceid}' already defined"; + my $d = $modules{$hash->{TYPE}}{defptr}{"D$device->{deviceid}"}; + $d->{association} = $device->{association} if($device->{association}); + + Log3 $name, 2, "$name: device '$device->{deviceid}' already defined"; next; } - my $detail = withings_getDeviceDetail( $hash, $device->{deviceid} ); + + my $detail = $device->{deviceproperties}; + next if( !defined($detail->{id}) ); my $id = $detail->{id}; my $devname = "withings_D". $id; my $define= "$devname withings $id"; - Log3 $name, 3, "$name: create new device '$devname' for device '$id'"; + Log3 $name, 2, "$name: create new device '$devname' for device '$id'"; my $cmdret= CommandDefine(undef,$define); if($cmdret) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret"; } else { $cmdret= CommandAttr(undef,"$devname alias ".$device_types{$detail->{type}}) if( defined($device_types{$detail->{type}}) ); - $cmdret= CommandAttr(undef,"$devname room withings"); + $cmdret= CommandAttr(undef,"$devname alias ".$device_models{$detail->{type}}->{$detail->{model}}) if( defined($device_models{$detail->{type}}) && defined($device_models{$detail->{type}}->{$detail->{model}}) ); + #$cmdret= CommandAttr(undef,"$devname room WithingsTest"); $cmdret= CommandAttr(undef,"$devname IODev $name"); + #$cmdret= CommandAttr(undef,"$devname disable 1"); + #$cmdret= CommandAttr(undef,"$devname verbose 5"); $autocreated++; } @@ -290,80 +724,124 @@ withings_connect($) CommandSave(undef,undef) if( $autocreated && AttrVal( "autocreate", "autosave", 1 ) ); } -sub -withings_initDevice($) -{ + + + + +sub withings_initDevice($) { my ($hash) = @_; my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: initdevice ".$hash->{Device}; AssignIoPort($hash); if(defined($hash->{IODev}->{NAME})) { - Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME}; + Log3 $name, 2, "$name: I/O device is " . $hash->{IODev}->{NAME}; } else { Log3 $name, 1, "$name: no I/O device"; } - my $device = withings_getDeviceDetail( $hash, $hash->{Device} ); + $hash->{'.https'} = "https"; + $hash->{'.https'} = "http" if( AttrVal($hash->{NAME}, "nossl", 0) ); + + my $device = withings_getDeviceDetail( $hash ); $hash->{DeviceType} = "UNKNOWN"; $hash->{sn} = $device->{sn}; $hash->{fw} = $device->{fw}; + $hash->{created} = $device->{created}; + $hash->{location} = $device->{latitude}.",".$device->{longitude} if(defined($device->{latitude})); $hash->{DeviceType} = $device->{type}; - $hash->{DeviceType} = $device_types{$device->{type}} if( defined($device_types{$device->{type}}) ); + $hash->{DeviceType} = $device_types{$device->{type}} if( defined($device->{type}) && defined($device_types{$device->{type}}) ); $hash->{model} = $device->{model}; $hash->{model} = $device_models{$device->{type}}->{$device->{model}} - if( defined($device_models{$device->{type}}) && defined($device_models{$device->{type}}->{$device->{model}}) ); + if( defined($device->{type}) && defined($device->{model}) && defined($device_models{$device->{type}}) && defined($device_models{$device->{type}}->{$device->{model}}) ); + $hash->{modelID} = $device->{model}; + $hash->{typeID} = $device->{type}; + $hash->{lastsessiondate} = $device->{lastsessiondate} if( defined($device->{lastsessiondate}) ); + $hash->{lastweighindate} = $device->{lastweighindate} if( defined($device->{lastweighindate}) ); - if( !defined( $attr{$name}{stateFormat} ) ) { - $attr{$name}{stateFormat} = "batteryLevel %"; - $attr{$name}{stateFormat} = "co2 ppm" if( $device->{type} == 1 && $device->{model} == 4 ); + if((defined($hash->{typeID}) && $hash->{typeID} == 16) or (defined($hash->{modelID}) && $hash->{modelID} == 61)) + { + my $devicelink = withings_getDeviceLink( $hash ); + if(defined($devicelink->{userid})) + { + $hash->{User} = $devicelink->{userid}; + $hash->{UserDevice} = $modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{userid}} if defined($modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{userid}}); + } } - withings_poll($hash); + + if( !defined( $attr{$name}{stateFormat} ) ) { + $attr{$name}{stateFormat} = "batteryLevel %"; + + $attr{$name}{stateFormat} = "co2 ppm" if( $device->{model} == 4 ); + $attr{$name}{stateFormat} = "voc ppm" if( $device->{model} == 22 ); + $attr{$name}{stateFormat} = "light lux" if( $device->{model} == 60 ); + $attr{$name}{stateFormat} = "lastWeighinDate" if( $device->{model} == 61 ); + } + + withings_readAuraAlarm($hash) if( defined(AttrVal($name,"IP",undef)) && defined($device->{model}) && $device->{model} == 60 && defined($device->{type}) && $device->{type} == 32 ); + + InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0); } -sub -withings_initUser($) -{ + +sub withings_initUser($) { my ($hash) = @_; my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: inituser ".$hash->{User}; AssignIoPort($hash); if(defined($hash->{IODev}->{NAME})) { - Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME}; + Log3 $name, 2, "$name: I/O device is " . $hash->{IODev}->{NAME}; } else { Log3 $name, 1, "$name: no I/O device"; } - my $user = withings_getUserDetail( $hash, $hash->{User} ); + $hash->{'.https'} = "https"; + $hash->{'.https'} = "http" if( AttrVal($hash->{NAME}, "nossl", 0) ); + + my $user = withings_getUserDetail( $hash ); $hash->{shortName} = $user->{shortname}; $hash->{gender} = ($user->{gender}==0)?"male":"female" if( defined($hash->{gender}) ); $hash->{userName} = ($user->{firstname}?$user->{firstname}:"") ." ". ($user->{lastname}?$user->{lastname}:""); $hash->{birthdate} = strftime("%Y-%m-%d", localtime($user->{birthdate})) if( defined($user->{birthdate}) ); + $hash->{created} = $user->{created}; + $hash->{modified} = $user->{modified}; $attr{$name}{stateFormat} = "weight kg" if( !defined( $attr{$name}{stateFormat} ) ); - withings_poll($hash); + InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0); } -sub -withings_getUsers($) -{ + + +sub withings_getUsers($) { my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getusers"; withings_getSessionKey($hash); - my $URL = 'http://healthmate.withings.com/index/service/account?action=getuserslist&sessionid='.$hash->{SessionKey}; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/account", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{SessionKey}, accountid => $hash->{AccountID} , recurse_use => '1', recurse_devtype => '1', listmask => '5', allusers => 't' , appname => 'my2', appliver=> $hash->{appliver}, apppfm => 'web', action => 'getuserslist'}, + }); + + #my $ua = LWP::UserAgent->new; + #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://healthmate.withings.com/index/service/account'); + #my $get_data = 'sessionid='.$hash->{SessionKey}.'&accountid='.$hash->{AccountID}.'&recurse_use=1&recurse_devtype=1&listmask=5&allusers=t&appname=my2&appliver='.$hash->{appliver}.'&apppfm=web&action=getuserslist'; + #$request->content($get_data); + #my $response = $ua->request($request); + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); my @users = (); foreach my $user (@{$json->{body}{users}}) { @@ -374,230 +852,1462 @@ withings_getUsers($) return \@users; } -sub -withings_getDevices($) -{ + + + +sub withings_getDevices($) { my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getdevices"; withings_getSessionKey($hash); - my $URL = 'http://healthmate.withings.com/index/service/association?action=getbyaccountid&sessionid='.$hash->{SessionKey}.'&accountid='.$hash->{AccountID}; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/association", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{SessionKey}, accountid => $hash->{AccountID} , type => '-1', enrich => 't' , appname => 'my2', appliver=> $hash->{appliver}, apppfm => 'web', action => 'getbyaccountid'}, + }); + + #my $ua = LWP::UserAgent->new; + #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://healthmate.withings.com/index/service/association'); + #my $get_data = 'sessionid='.$hash->{SessionKey}.'&accountid='.$hash->{AccountID}.'&type=-1&enrich=t&appname=my2&appliver='.$hash->{appliver}.'&apppfm=web&action=getbyaccountid'; + #$request->content($get_data); + #my $response = $ua->request($request); + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ );; + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ );; + Log3 "withings", 5, "$name: getdevices ".Dumper($json); my @devices = (); foreach my $association (@{$json->{body}{associations}}) { next if( !defined($association->{deviceid}) ); - push( @devices, $association ); } - return \@devices; } -sub -withings_getDeviceDetail($$) -{ - my ($hash,$id) = @_; - $hash = $hash->{IODev} if( defined($hash->{IODev}) ); - withings_getSessionKey( $hash ); - my $URL = 'http://healthmate.withings.com/index/service/device?action=getproperties&sessionid='.$hash->{SessionKey}.'&deviceid='.$id; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + +sub withings_getDeviceDetail($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getdevicedetail ".$hash->{Device}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/device", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid => $hash->{Device} , appname => 'my2', appliver=> $hash->{IODev}->{appliver}, apppfm => 'web', action => 'getproperties'}, + }); + + #Log3 "withings", 5, "$name: getdevicedetaildata ".Dumper($data); + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); + + if($json) + { + my $device = $json->{body}; + $hash->{sn} = $device->{sn}; + $hash->{fw} = $device->{fw}; + $hash->{created} = $device->{created}; + $hash->{location} = $device->{latitude}.",".$device->{longitude} if(defined($device->{latitude})); + $hash->{DeviceType} = $device->{type}; + $hash->{DeviceType} = $device_types{$device->{type}} if( defined($device->{type}) && defined($device_types{$device->{type}}) ); + $hash->{model} = $device->{model}; + $hash->{model} = $device_models{$device->{type}}->{$device->{model}} + if( defined($device->{type}) && defined($device->{model}) && defined($device_models{$device->{type}}) && defined($device_models{$device->{type}}->{$device->{model}}) ); + $hash->{modelID} = $device->{model}; + $hash->{typeID} = $device->{type}; + $hash->{lastsessiondate} = $device->{lastsessiondate} if( defined($device->{lastsessiondate}) ); + $hash->{lastweighindate} = $device->{lastweighindate} if( defined($device->{lastweighindate}) ); + } return $json->{body}; } -sub -withings_getDeviceReadings($$) -{ - my ($hash,$id) = @_; + + +sub withings_getDeviceLink($) { + my ($hash) = @_; my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getdevicelink ".$hash->{Device}; return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); - $hash = $hash->{IODev} if( defined($hash->{IODev}) ); + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/v2/link", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid => $hash->{Device} , appname => 'my2', appliver=> $hash->{IODev}->{appliver}, apppfm => 'web', action => 'get'}, + }); - withings_getSessionKey( $hash ); - - my $lastupdate = ReadingsVal( $name, ".lastupdate", undef ); - - my $URL = 'http://healthmate.withings.com/index/service/v2/measure?action=getmeashf&meastype=12%2C35&sessionid='.$hash->{SessionKey}.'&deviceid='.$id; - $URL .= "&lastupdate=$lastupdate" if( $lastupdate ); - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + #my $ua = LWP::UserAgent->new; + #my $request = HTTP::Request->new(POST => $hash->{'.https'}.'://healthmate.withings.com/index/service/v2/link'); + #my $get_data = 'sessionid='.$hash->{IODev}->{SessionKey}.'&deviceid='.$hash->{Device}.'&appname=my2&appliver='.$hash->{IODev}->{appliver}.'&apppfm=web&action=get'; + #$request->content($get_data); + #my $response = $ua->request($request); + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); + + return $json->{body}; +} + + + + + +sub withings_getDeviceProperties($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 "withings", 5, "$name: getdeviceproperties ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/device", + timeout => 30, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, appname => 'my2', appliver => $hash->{IODev}->{appliver}, apppfm => 'web', action => 'getproperties'}, + hash => $hash, + type => 'deviceProperties', + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollProperties", $seconds, 0 ); + + return undef; + + + + +} + +sub withings_getDeviceReadingsScale($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getscalereadings ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(24*60*60)); + $enddate = $now if ($enddate > $now); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 30, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, meastype => '12,35', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{appliver}, apppfm => 'web', action => 'getmeashf'}, + hash => $hash, + type => 'deviceReadingsScale', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + +} + + +sub withings_getDeviceReadingsBedside($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getaurareadings ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(8*60*60)); + $enddate = $now if ($enddate > $now); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 30, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getmeashf'}, + hash => $hash, + type => 'deviceReadingsBedside', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + +} + + +sub withings_getDeviceReadingsHome($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: gethomereadings ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(8*60*60)); + $enddate = $now if ($enddate > $now); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 30, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getmeashf'}, + hash => $hash, + type => 'deviceReadingsHome', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + +} + + +sub withings_getDeviceEventsBaby($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getbabyevents ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/event", + timeout => 30, + noshutdown => 1, + data => {activated => '0', action => 'get', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, type => '10,11,12,13,14,15,20', begindate => int($lastupdate)}, + hash => $hash, + type => 'deviceReadingsBaby', + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + + +} + + + +sub withings_getDeviceAlertsHome($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: gethomealerts ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastAlert", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + + HttpUtils_NonblockingGet({ + url => "https://scalews.withings.net/cgi-bin/v2/timeline", + timeout => 30, + noshutdown => 1, + data => {type => '1', callctx => 'foreground', action => 'getbydeviceid', appname => 'HomeMonitor', apppfm => 'ios', appliver => '20000', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, lastupdate => int($lastupdate) }, + hash => $hash, + type => 'deviceAlertsHome', + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollAlert", $seconds, 0 ); + + return undef; + + +} + + +sub withings_getDeviceAlertsBaby($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getbabyevents ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastAlert", ($now-120*60) ); + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/event", + timeout => 30, + noshutdown => 1, + data => {activated => '1', action => 'get', sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, type => '10,11,12,13,14,15,20', begindate => int($lastupdate)}, + hash => $hash, + type => 'deviceAlertsBaby', + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollAlert", $seconds, 0 ); + + return undef; + + +} + +sub withings_getVideoLink($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getbabyvideo ".$hash->{Device}; + return undef if( !defined($hash->{Device}) ); + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://babyws.withings.net/cgi-bin/presence", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid => $hash->{Device} , action => 'get'}, + }); + return undef if(!defined($data)); + + my $json = (); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); + + if(defined($json->{body}{device})) + { + $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}."/"; + $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}."/"; + } + return $json; + +} + +sub withings_getS3Credentials($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + return undef if( !defined($hash->{Device}) ); + + return undef if( $hash->{sts_expiretime} && $hash->{sts_expiretime} > time - 3600 ); # min 1h + + return undef if( !defined($hash->{IODev}) ); + Log3 "withings", 5, "$name: gets3credentials ".$hash->{Device}; + + withings_getSessionKey( $hash->{IODev} ); + + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://scalews.withings.net/cgi-bin/v2/device", + timeout => 10, + noshutdown => 1, + data => {callctx => 'foreground', action => 'getsts', deviceid => $hash->{Device}, appname => 'HomeMonitor', apppfm => 'ios' , appliver => '20000', sessionid => $hash->{IODev}->{SessionKey}}, + }); + return undef if(!defined($data)); + + my $json = (); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); + + if(defined($json->{body}{sts})) + { + $hash->{sts_region} = $json->{body}{sts}{region}; + $hash->{sts_sessiontoken} = $json->{body}{sts}{sessiontoken}; + $hash->{sts_accesskeyid} = $json->{body}{sts}{accesskeyid}; + $hash->{sts_expiretime} = $json->{body}{sts}{expiretime}; + $hash->{sts_secretaccesskey} = $json->{body}{sts}{secretaccesskey}; + $hash->{sts_buckets} = (@{$json->{body}{sts}{buckets}}).join(","); + } return $json; + } -sub -withings_getUserDetail($$) -{ - my ($hash,$id) = @_; + +sub withings_signS3Link($$$;$) { + my ($hash,$url,$sign,$bucket) = @_; + my $name = $hash->{NAME}; + + withings_getS3Credentials($hash); + + my $signing = "GET\n\n\n"; + $signing .= $hash->{sts_expiretime}."\n"; + $signing .= "x-amz-security-token:".$hash->{sts_sessiontoken}."\n"; + $signing .= $sign; + + my $signature = hmac_sha1_base64($signing, $hash->{sts_secretaccesskey})."="; + + $url .= "?AWSAccessKeyId=".uri_escape($hash->{sts_accesskeyid}); + $url .= "&Expires=".$hash->{sts_expiretime}; + $url .= "&x-amz-security-token=".uri_escape($hash->{sts_sessiontoken}); + $url .= "&Signature=".uri_escape($signature); + + return $url; +} + +sub withings_getUserDetail($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getuserdetails ".$hash->{User}; + return undef if( !defined($hash->{User}) ); return undef if( $hash->{SUBTYPE} ne "USER" ); - my $URL = "http://wbsapi.withings.net/user?action=getbyuserid&userid=$hash->{User}&publickey=$hash->{Key}"; - my $agent = LWP::UserAgent->new(env_proxy => 1,keep_alive => 1, timeout => 30); - my $header = HTTP::Request->new(GET => $URL); - my $request = HTTP::Request->new('GET', $URL, $header); - my $response = $agent->request($request); + return undef if( !defined($hash->{IODev})); + withings_getSessionKey( $hash->{IODev} ); + my ($err,$data) = HttpUtils_BlockingGet({ + url => $hash->{'.https'}."://healthmate.withings.com/index/service/user", + timeout => 10, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, userid => $hash->{User} , appname => 'my2', appliver => $hash->{IODev}->{appliver}, apppfm => 'web', action => 'getbyuserid'}, + }); + + return undef if(!defined($data)); my $json = (); - $json = JSON->new->utf8(0)->decode($response->content) if( $response->content =~ m/^{.*}$/ ); + $json = JSON->new->utf8(0)->decode($data) if( $data =~ m/^{.*}$/ ); return $json->{body}{users}[0]; } -sub -withings_poll($) -{ - my ($hash) = @_; + + +sub withings_poll($;$) { + my ($hash,$force) = @_; + $force = 0 if(!defined($force)); my $name = $hash->{NAME}; RemoveInternalTimer($hash); - if( $hash->{SUBTYPE} eq "DEVICE" ) { - withings_pollDevice($hash); - } elsif( $hash->{SUBTYPE} eq "USER" ) { - withings_pollUser($hash); + return undef if(AttrVal($name,"disable",0) eq "1"); + + + my $resolve = inet_aton("healthmate.withings.com"); + if(!defined($resolve)) + { + $hash->{STATE} = "DNS error"; + InternalTimer( gettimeofday() + 3600, "withings_poll", $hash, 0); + return undef; } - InternalTimer(gettimeofday()+$hash->{INTERVAL}, "withings_poll", $hash, 0); + + + my ($now) = int(time()); + + if( $hash->{SUBTYPE} eq "DEVICE" ) { + my $intervalData = AttrVal($name,"intervalData",900); + my $intervalDebug = AttrVal($name,"intervalDebug",900); + my $lastData = ReadingsVal( $name, ".pollData", 0 ); + my $lastDebug = ReadingsVal( $name, ".pollDebug", 0 ); + my $intervalProperties = AttrVal($name,"intervalProperties",43200); + my $lastProperties = ReadingsVal( $name, ".pollProperties", 0 ); + + if(defined($hash->{modelID}) && $hash->{modelID} eq '4') { + withings_getDeviceProperties($hash) if($force > 1 || $lastData <= ($now - $intervalData)); + withings_getDeviceReadingsScale($hash) if($force || $lastData <= ($now - $intervalData)); + } + elsif(defined($hash->{modelID}) && $hash->{modelID} eq '21') { + my $intervalAlert = AttrVal($name,"intervalAlert",120); + my $lastAlert = ReadingsVal( $name, ".pollAlert", 0 ); + withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); + withings_getDeviceEventsBaby($hash) if($force || $lastData <= ($now - $intervalData)); + #withings_getDeviceAlertsBaby($hash) if($force || $lastAlert <= ($now - $intervalAlert)); + } + elsif(defined($hash->{modelID}) && $hash->{modelID} eq '22') { + my $intervalAlert = AttrVal($name,"intervalAlert",120); + my $lastAlert = ReadingsVal( $name, ".pollAlert", 0 ); + withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); + withings_getDeviceReadingsHome($hash) if($force || $lastData <= ($now - $intervalData)); + withings_getDeviceAlertsHome($hash) if($force || $lastAlert <= ($now - $intervalAlert)); + } + elsif(defined($hash->{typeID}) && $hash->{typeID} eq '16') { + withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); + withings_getUserReadingsActivity($hash) if($force || $lastData <= ($now - $intervalData)); + } + elsif(defined($hash->{modelID}) && $hash->{modelID} eq '60') { + withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); + withings_getDeviceReadingsBedside($hash) if($force || $lastData <= ($now - $intervalData)); + } + elsif(defined($hash->{modelID}) && $hash->{modelID} eq '61') { + withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); + withings_getUserReadingsSleep($hash) if($force || $lastData <= ($now - $intervalData)); + withings_getUserReadingsSleepDebug($hash) if($force || $lastDebug <= ($now - $intervalDebug)); + } + else + { + withings_getDeviceProperties($hash) if($force || $lastProperties <= ($now - $intervalProperties)); + } + } elsif( $hash->{SUBTYPE} eq "DUMMY" ) { + my $intervalData = AttrVal($name,"intervalData",900); + my $lastData = ReadingsVal( $name, ".pollData", 0 ); + if($hash->{typeID} eq '16') { + withings_getUserReadingsActivity($hash) if($force || $lastData <= ($now - $intervalData)); + } + } elsif( $hash->{SUBTYPE} eq "USER" ) { + + my $intervalData = AttrVal($name,"intervalData",900); + my $intervalDaily = AttrVal($name,"intervalDaily",(6*60*60)); + my $lastData = ReadingsVal( $name, ".pollData", 0 ); + my $lastDaily = ReadingsVal( $name, ".pollDaily", 0 ); + + withings_getUserReadingsCommon($hash) if($force || $lastData <= ($now - $intervalData)); + withings_getUserReadingsDaily($hash) if($force || $lastDaily <= ($now - $intervalDaily)); + } + + InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0); } -sub -withings_pollDevice($) -{ + + + + +sub withings_getUserReadingsDaily($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getuserdailystats ".$hash->{User}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastAggregate", ($now-21*24*60*60) );#$hash->{created} );# + my $enddate = ($lastupdate+(14*24*60*60)); + $enddate = $now if ($enddate > $now); + + my $startdateymd = strftime("%Y-%m-%d", localtime($lastupdate)); + my $enddateymd = strftime("%Y-%m-%d", localtime($enddate)); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/aggregate", + timeout => 60, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getbyuserid'}, + hash => $hash, + type => 'userDailyAggregate', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + $lastupdate = ReadingsVal( $name, ".lastActivity", ($now-21*24*60*60) );#$hash->{created} ); + $enddate = ($lastupdate+(14*24*60*60)); + $enddate = $now if ($enddate > $now); + + $startdateymd = strftime("%Y-%m-%d", localtime($lastupdate)); + $enddateymd = strftime("%Y-%m-%d", localtime($enddate)); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/activity", + timeout => 60, + noshutdown => 1, + data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, subcategory => '37', startdateymd => $startdateymd, enddateymd => $enddateymd, appname => 'my2', appliver => $hash->{IODev}->{appliver}, apppfm => 'web', action => 'getbyuserid'}, + hash => $hash, + type => 'userDailyActivity', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollDaily", $seconds, 0 ); + + return undef; + + +} + + + +sub withings_getUserReadingsCommon($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getuserreadings ".$hash->{User}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-100*24*60*60) );#$hash->{created} );# + my $enddate = ($lastupdate+(100*24*60*60)); + $enddate = $now if ($enddate > $now); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/measure", + timeout => 60, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getmeas'}, + hash => $hash, + type => 'userReadingsCommon', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + + +} + + + + +sub withings_getUserReadingsSleep($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getsleepreadings ".$hash->{User}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(8*60*60)); + $enddate = $now if ($enddate > $now); + # 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}->{appliver}, apppfm => 'web', action => 'getvasistas'}, + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 60, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getvasistas'}, + hash => $hash, + type => 'userReadingsSleep', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + +} + + +sub withings_getUserReadingsSleepDebug($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 "withings", 5, "$name: getsleepreadingsdebug ".$hash->{User}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastDebug", ($now-7*24*60*60) );#$hash->{created} ); + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(8*60*60)); + $enddate = $now if ($enddate > $now); + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 60, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getvasistas'}, + hash => $hash, + type => 'userReadingsSleepDebug', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollDebug", $seconds, 0 ); + + return undef; + +} + + + +sub withings_getUserReadingsActivity($) { my ($hash) = @_; my $name = $hash->{NAME}; - my $json = withings_getDeviceReadings( $hash, $hash->{Device} ); - if( $json ) { + Log3 "withings", 5, "$name: getactivityreadings ".$hash->{User}; + + return undef if( !defined($hash->{IODev}) ); + withings_getSessionKey( $hash->{IODev} ); + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", ($now-7*24*60*60) );#$hash->{created} );# + $lastupdate = $hash->{lastsessiondate} if(defined($hash->{lastsessiondate}) and $hash->{lastsessiondate} < $lastupdate); + my $enddate = ($lastupdate+(8*60*60)); + $enddate = $now if ($enddate > $now); + + Log3 "withings", 5, "$name: getactivityreadings ".$lastupdate." to ".$enddate; + + HttpUtils_NonblockingGet({ + url => "https://healthmate.withings.com/index/service/v2/measure", + timeout => 60, + noshutdown => 1, + 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}->{appliver}, apppfm => 'web', action => 'getvasistas'}, + hash => $hash, + type => 'userReadingsActivity', + enddate => int($enddate), + callback => \&withings_Dispatch, + }); + + my ($seconds) = gettimeofday(); + $hash->{LAST_POLL} = FmtDateTime( $seconds ); + readingsSingleUpdate( $hash, ".pollData", $seconds, 0 ); + + return undef; + + +} + + + +sub withings_parseProperties($$) { + my ($hash,$json) = @_; + my $name = $hash->{NAME}; + + Log3 "withings", 5, "$name: parsedevice"; + + #parse + my $detail = $json->{body}; + + readingsBeginUpdate($hash); + + if( defined($detail->{batterylvl}) and $detail->{batterylvl} > 0 and $detail->{type} ne '32' and $detail->{model} ne '22') { + readingsBulkUpdate( $hash, "batteryLevel", $detail->{batterylvl}, 1 ); + readingsBulkUpdate( $hash, "battery", ($detail->{batterylvl}>20?"ok":"low"), 1 ); + } + readingsBulkUpdate( $hash, "lastWeighinDate", FmtDateTime($detail->{lastweighindate}), 1 ) if( defined($detail->{lastweighindate}) and $detail->{lastweighindate} > 0 and $detail->{model} ne '60' ); + readingsBulkUpdate( $hash, "lastSessionDate", FmtDateTime($detail->{lastsessiondate}), 1 ) if( defined($detail->{lastsessiondate}) ); + $hash->{lastsessiondate} = $detail->{lastsessiondate} if( defined($detail->{lastsessiondate}) ); + + readingsEndUpdate($hash,1); + +} + +sub withings_parseMeasureGroups($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parsemeasuregroups"; + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", 0 ); + my $newlastupdate = $lastupdate; + $hash->{status} = $json->{status}; - my $lastupdate = ReadingsVal( $name, ".lastupdate", 0 ); - my @readings = (); if( $hash->{status} == 0 ) { + my $i = 0; + + foreach my $measuregrp ( sort { $a->{date} <=> $b->{date} } @{$json->{body}{measuregrps}}) { + if( $measuregrp->{date} < $newlastupdate ) + { + Log3 $name, 4, "$name: old measuregroup skipped: ".FmtDateTime($measuregrp->{date}); + next; + } + + $newlastupdate = $measuregrp->{date}; + + foreach my $measure (@{$measuregrp->{measures}}) { + my $reading = $measure_types{$measure->{type}}->{reading}; + if( !defined($reading) ) { + Log3 $name, 1, "$name: unknown measure type: $measure->{type}"; + next; + } + + my $value = $measure->{value} * 10 ** $measure->{unit}; + + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($measuregrp->{date}); + readingsBulkUpdate( $hash, $reading, $value, 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($measuregrp->{date}); + readingsEndUpdate($hash,1); + } + } + + + if($newlastupdate == $lastupdate and $i == 0) + { + my $user = withings_getUserDetail( $hash ); + $hash->{modified} = $user->{modified}; + + $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate}); + $newlastupdate = $user->{modified} if($user->{modified} and $user->{modified} < $newlastupdate); + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Measuregroups gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsSingleUpdate( $hash, ".lastData", $newlastupdate+1, 0 ); + $hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from MeasureGroups (latest: '.FmtDateTime($newlastupdate).')'; + + } + +} + +sub withings_parseMeasurements($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parsemeasurements"; + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", 0 ); + my $newlastupdate = $lastupdate; + my $i = 0; + + if( $json ) + { + $hash->{status} = $json->{status}; + + my @readings = (); + if( $hash->{status} == 0 ) + { + foreach my $series ( @{$json->{body}{series}}) { my $reading = $measure_types{$series->{type}}->{reading}; if( !defined($reading) ) { - Log3 $name, 3, "$name: unknown measure type: $series->{type}"; + Log3 $name, 1, "$name: unknown measure type: $series->{type}"; next; } foreach my $measure (@{$series->{data}}) { - next if( $measure->{date} < $lastupdate ); my $value = $measure->{value}; - push(@readings, [$measure->{date}, $reading, $value]); } } if( @readings ) { - readingsBeginUpdate($hash); - my $i = 0; + $i = 0; foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings) { + if( $reading->[0] < $newlastupdate ) + { + Log3 $name, 5, "$name: old measurement skipped: ".FmtDateTime($reading->[0])." ".$reading->[1]; + next; + } + $newlastupdate = $reading->[0]; + + readingsBeginUpdate($hash); $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]); - $hash->{CHANGETIME}[$i++] = FmtDateTime($reading->[0]); readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]);; + readingsEndUpdate($hash,1); + } - my ($seconds) = gettimeofday(); - $hash->{LAST_POLL} = FmtDateTime( $seconds ); - readingsBulkUpdate( $hash, ".lastupdate", $seconds, 0 ); - readingsEndUpdate($hash,1); - - delete $hash->{CHANGETIME}; } + + if($newlastupdate == $lastupdate and $i == 0) + { + my $device = withings_getDeviceDetail( $hash ); + $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate}); + $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate); + $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate); + + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Measurements gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsSingleUpdate( $hash, ".lastData", $newlastupdate+1, 0 ); + $hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from Measurements (latest: '.FmtDateTime($newlastupdate).')'; + + } } - readingsBeginUpdate($hash); - my $detail = withings_getDeviceDetail( $hash, $hash->{Device} ); - if( defined($detail->{batterylvl}) ) { - readingsBulkUpdate( $hash, "batteryLevel", $detail->{batterylvl}, 1 ); - readingsBulkUpdate( $hash, "battery", ($detail->{batterylvl}>20?"ok":"low"), 1 ); - } - readingsBulkUpdate( $hash, "lastWeighinDate", FmtDateTime($detail->{lastweighindate}), 1 ) if( defined($detail->{lastweighindate}) ); - - readingsEndUpdate($hash,1); } -sub -withings_pollUser($) -{ - my ($hash) = @_; + + +sub withings_parseAggregate($$) { + my ($hash, $json) = @_; my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parseaggregate"; - my $lastupdate = ReadingsVal( $name, ".lastupdate", undef ); + #return undef; + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastAggregate", 0 ); + my $newlastupdate = $lastupdate; + my $i = 0; + my $unfinished; - my $url = "http://wbsapi.withings.net/measure?action=getmeas"; - $url .= "&userid=$hash->{User}&publickey=$hash->{Key}"; - $url .= "&lastupdate=$lastupdate" if( $lastupdate ); - my $ret = get($url); + if( $json ) + { + $hash->{status} = $json->{status}; - return if( !$ret ); + my @readings = (); + if( $hash->{status} == 0 ) + { - #my $json = JSON->new->utf8(0)->decode($ret); - my $json = (); - $json = JSON->new->utf8->decode(encode('UTF-8', $ret)) if( $ret =~ m/^{.*}$/ ); + if(defined($json->{body}{series})) + { - return undef if( !$json ); + foreach my $series ( keys(%{$json->{body}{series}})) + { - $hash->{status} = $json->{status}; - if( $hash->{status} == 0 ) { - my $i = 0; - readingsBeginUpdate($hash); - foreach my $measuregrp ( sort { $a->{date} <=> $b->{date} } @{$json->{body}{measuregrps}}) { - foreach my $measure (@{$measuregrp->{measures}}) { - my $reading = $measure_types{$measure->{type}}->{reading}; - if( !defined($reading) ) { - Log3 $name, 3, "$name: unknown measure type: $measure->{type}"; + if(defined($json->{body}{series}{$series})) + { + my $typestring = substr($series, -2); + + foreach my $dayvalues ( keys ($json->{body}{series}{$series})) + { + if(!$json->{body}{series}{$series}{$dayvalues}->{complete}) + { + $unfinished = 1; + next; + } + + + my ($year,$mon,$day) = split(/[\s-]+/, $dayvalues); + my $timestamp = timelocal(0,0,18,$day,$mon-1,$year-1900); + #my $timestamp = $json->{body}{series}{$series}{$dayvalues}->{midnight}; + my $reading = $measure_types{$typestring}->{dailyreading}; + if( !defined($reading) ) { + Log3 $name, 1, "$name: unknown measure type: $typestring"; + next; + } + my $value = $json->{body}{series}{$series}{$dayvalues}->{sum}; + + push(@readings, [$timestamp, $reading, $value]); + } + } + } + } + + if( @readings ) + { + $i = 0; + foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings) + { + if( $reading->[0] < $newlastupdate ) + { + Log3 $name, 5, "$name: old aggregate skipped: ".FmtDateTime($reading->[0])." ".$reading->[1]; + next; + } + + $newlastupdate = $reading->[0]; + + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]); + readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]); + readingsEndUpdate($hash,1); + + } + + } + + + if($newlastupdate == $lastupdate and $i == 0) + { + $newlastupdate = $lastupdate - 1; #$json->{requestedenddate} if($json->{requestedenddate}); + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Aggregate gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsSingleUpdate( $hash, ".lastAggregate", $newlastupdate+1, 0 ); + #$hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from Aggregate (latest: '.FmtDateTime($newlastupdate).')'; + + + } + } + + + +} + + +sub withings_parseActivity($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parseactivity"; + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastActivity", 0 ); + my $newlastupdate = $lastupdate; + my $i = 0; + my $unfinished; + + if( $json ) + { + $hash->{status} = $json->{status}; + + my @readings = (); + if( $hash->{status} == 0 ) + { + + foreach my $series ( @{$json->{body}{series}}) + { + if($series->{completed} ne '1') + { + $unfinished = 1; next; } - my $value = $measure->{value} * 10 ** $measure->{unit}; + foreach my $dataset ( keys (%{$series->{data}})) + { + if(!defined($sleep_readings{$dataset}->{reading})) + { + Log3 $name, 2, "$name: unknown sleep reading $dataset"; + next; + } + + my ($year,$mon,$day) = split(/[\s-]+/, $series->{date}); + my $timestamp = timelocal(0,0,6,$day,$mon-1,$year-1900); + my $reading = $sleep_readings{$dataset}->{reading}; + my $value = $series->{data}{$dataset}; + + push(@readings, [$timestamp, $reading, $value]); + } + + } + + + if( @readings ) { + $i = 0; + foreach my $reading (sort { $a->[0] <=> $b->[0] } @readings) { + if( $reading->[0] < $newlastupdate ) + { + Log3 $name, 5, "$name: old activity skipped: ".FmtDateTime($reading->[0])." ".$reading->[1]; + next; + } + + $newlastupdate = $reading->[0]; + + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($reading->[0]); + readingsBulkUpdate( $hash, $reading->[1], $reading->[2], 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($reading->[0]); + readingsEndUpdate($hash,1); + + } - $hash->{".updateTimestamp"} = FmtDateTime($measuregrp->{date}); - $hash->{CHANGETIME}[$i++] = FmtDateTime($measuregrp->{date}); - readingsBulkUpdate( $hash, $reading, $value, 1 ); } + + if($newlastupdate == $lastupdate and $i == 0) + { + $newlastupdate = $lastupdate - 1; #$json->{requestedenddate} if($json->{requestedenddate}); + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Activity gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") .$i if($i>0)"; + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsSingleUpdate( $hash, ".lastActivity", $newlastupdate+1, 0 ); + #$hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from Activity (latest: '.FmtDateTime($newlastupdate).')'; + + } - - my ($seconds) = gettimeofday(); - $hash->{LAST_POLL} = FmtDateTime( $seconds ); - - readingsBulkUpdate( $hash, ".lastupdate", $seconds, 0 ); - - readingsEndUpdate($hash,1); - - delete $hash->{CHANGETIME}; } + + + } -sub -withings_Get($$@) -{ + +sub withings_parseWorkouts($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 1, "$name: parseworkouts\n".Dumper($json); + +return undef; + +} + + + + +sub withings_parseVasistas($$;$) { + my ($hash, $json, $datatype) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parsevasistas"; + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", 0 ); + $lastupdate = ReadingsVal( $name, ".lastDebug", 0 ) if($datatype =~ /Debug/); + + if( $json ) { + $hash->{status} = $json->{status}; + if( $hash->{status} == 0 ) { + my @readings = (); + my $i = 0; + my $j; + my $k; + my $readingsdate; + my $newlastupdate = $lastupdate; + + + + foreach my $series ( @{$json->{body}{series}}) { + $j=0; + my @types= (@{$series->{types}}); + my @dates= (@{$series->{dates}}); + my @values= (@{$series->{vasistas}}); + + foreach $readingsdate (@dates) { + my @readingsvalue = (@{$values[$j++]}); + if($readingsdate <= $lastupdate) + { + Log3 $name, 4, "$name: old vasistas skipped: ".FmtDateTime($readingsdate); + next; + } + + $k=0; + foreach my $readingstype (@types) { + + my $updatetime = FmtDateTime($readingsdate); + my $updatevalue = $readingsvalue[$k++]; + my $updatetype = $measure_types{$readingstype}->{reading}; + if( !defined($updatetype) ) { + Log3 $name, 1, "$name: unknown measure type: $readingstype"; + next; + } + if(($updatetype eq "breathing") and ($updatevalue > 90)) { + Log3 $name, 2, "$name: Implausible Aura reading ".$updatetime.' '.$updatetype.': '.$updatevalue; + $newlastupdate = $readingsdate if($readingsdate > $newlastupdate); + next; + } + if($updatetype eq "duration") + { + Log3 $name, 1, "$name: Duration skipped ".$updatetime.' '.$updatetype.': '.$updatevalue if($updatevalue > 90); + $newlastupdate = $readingsdate if($readingsdate > $newlastupdate); + next; + } + if($updatetype eq "activityType") + { + my $activity = $updatevalue; + $updatevalue = $activity_types{$updatevalue}; + if( !defined($updatevalue) ) { + Log3 $name, 1, "$name: unknown activity type: $activity"; + $updatevalue = $activity; + } + } + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($readingsdate); + readingsBulkUpdate( $hash, $updatetype, $updatevalue, 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($readingsdate); + readingsEndUpdate($hash,1); + + if($updatetype ne "unknown") { + $newlastupdate = $readingsdate if($readingsdate > $newlastupdate); + } + + } + } + } + if($newlastupdate == $lastupdate and $i == 0) + { + my $device = withings_getDeviceDetail( $hash ); + $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate}); + $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate); + $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate); + + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < ($lastupdate-1)) + { + Log3 $name, 2, "$name: Vasistas gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate; + } + + my ($seconds) = gettimeofday(); + + if($datatype =~ /Debug/) + { + readingsSingleUpdate( $hash, ".lastDebug", $newlastupdate, 0 ); + } else { + readingsSingleUpdate( $hash, ".lastData", $newlastupdate, 0 ); + } + $hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + Log3 $name, 4, "$name: got ".$i.' entries from Vasistas (latest: '.FmtDateTime($newlastupdate).')'; + + } + } + + +} + + +sub withings_parseTimeline($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parsemetimeline "; + + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastAlert", 0 ); + my $newlastupdate = $lastupdate; + + $hash->{status} = $json->{status}; + if( $hash->{status} == 0 ) + { + my $i = 0; + + foreach my $event ( sort { $a->{epoch} <=> $b->{epoch} } @{$json->{body}{timeline}}) { + if( $event->{epoch} < $newlastupdate ) + { + Log3 $name, 5, "$name: old timeline event skipped: ".FmtDateTime($event->{epoch})." $event->{class}"; + next; + } + $newlastupdate = $event->{epoch}; + 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') + { + next; + } + elsif($event->{class} eq 'deleted') + { + Log3 "withings", 5, "withings: event " . FmtDateTime($event->{epoch})." Event was deleted"; + next; + } + 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') + { + Log3 "withings", 2, "withings: alert class unknown " . $event->{class}; + next; + } + + + my $reading = $timeline_classes{$event->{class}}->{reading}; + my $value = "alert"; + $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})); + + if( !defined($reading) ) { + Log3 $name, 2, "$name: unknown event type: $event->{class}"; + next; + } + else + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($event->{epoch}); + readingsBulkUpdate( $hash, $reading, $value, 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($event->{epoch}); + readingsEndUpdate($hash,1); + + } + if(AttrVal($name,"videoLinkEvents",0) eq "1") + { + my $pathlist = $event->{data}->{path_list}[0]; + my $eventurl = withings_signS3Link($hash,$pathlist->{url},$pathlist->{sign}); + DoTrigger($name, "alerturl: ".$eventurl); + } + } + + if($newlastupdate == $lastupdate and $i == 0) + { + my $device = withings_getDeviceDetail( $hash ); + $newlastupdate = $json->{requestedenddate} if(defined($json->{requestedenddate})); + $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate); + $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate); + + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Timeline gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsSingleUpdate( $hash, ".lastAlert", $newlastupdate+1, 0 ); + #$hash->{LAST_DATA} = FmtDateTime( $lastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from Timeline (latest: '.FmtDateTime($newlastupdate).')'; + + } + +} + + +sub withings_parseEvents($$) { + my ($hash, $json) = @_; + my $name = $hash->{NAME}; + #parse + Log3 "withings", 5, "$name: parseevents"; + my ($now) = time; + my $lastupdate = ReadingsVal( $name, ".lastData", 0 ); + my $lastalertupdate = ReadingsVal( $name, ".lastAlert", 0 ); + my $newlastupdate = $lastupdate; + + $hash->{status} = $json->{status}; + if( $hash->{status} == 0 ) { + my $i = 0; + foreach my $event ( sort { $a->{date} <=> $b->{date} } @{$json->{body}{events}}) { + if( $event->{date} < $newlastupdate ) + { + Log3 $name, 5, "$name: old event skipped: ".FmtDateTime($event->{date})." $event->{type}"; + next; + } + next if( $event->{deviceid} ne $hash->{Device} ); + $newlastupdate = $event->{date}; + + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($event->{date}); + my $changeindex = 0; + + #Log3 "withings", 5, "withings: event " . FmtDateTime($event->{date})." ".$event->{type}." ".$event->{activated}."/".$event->{measure}{value}; + + my $reading = $event_types{$event->{type}}->{reading}; + my $value = "notice"; + if($event->{activated}) + { + $lastalertupdate = $event->{date}; + $value = "alert"; + } + + if( !defined($reading) ) { + Log3 $name, 2, "$name: unknown event type: $event->{type}"; + next; + } + else + { + readingsBulkUpdate( $hash, $reading, $value, 1 ); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date}); + } + + if(defined($event->{duration}) and $event->{duration} ne "0") + { + my $durationreading = $event_types{$event->{type}}->{duration}; + my $durationvalue = $event->{duration}; + + readingsBulkUpdate( $hash, $durationreading, $durationvalue, 0 ); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date}); + + } + + if($event->{type} ne "20" and $event->{activated}) + { + my $thresholdreading = $event_types{$event->{type}}->{threshold}; + my $thresholdvalue = $event->{threshold}->{value} * 10 ** $event_types{$event->{type}}->{unit}; + + readingsBulkUpdate( $hash, $thresholdreading, $thresholdvalue, 0 ); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($event->{date}); + } + + + readingsEndUpdate($hash,1); + + } + + if($newlastupdate == $lastupdate and $i == 0) + { + my $device = withings_getDeviceDetail( $hash ); + $newlastupdate = $json->{requestedenddate} if($json->{requestedenddate}); + $newlastupdate = $device->{lastsessiondate} if($device->{lastsessiondate} and $device->{lastsessiondate} < $newlastupdate); + $newlastupdate = $device->{lastweighindate} if($device->{lastweighindate} and $device->{lastweighindate} < $newlastupdate); + + } + $newlastupdate = $now if($newlastupdate > $now); + if($newlastupdate < $lastupdate-1) + { + Log3 $name, 2, "$name: Events gap error! (latest: ".FmtDateTime($newlastupdate)." < ".FmtDateTime($lastupdate-1).") ".$i if($i>0); + withings_getDeviceProperties($hash) if($i>0); + $newlastupdate = $lastupdate-1; + } + + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, ".lastAlert", $lastalertupdate, 0 ); + readingsBulkUpdate( $hash, ".lastData", $newlastupdate+1, 0 ); + readingsEndUpdate($hash,0); + + $hash->{LAST_DATA} = FmtDateTime( $newlastupdate ); + + + delete $hash->{CHANGETIME}; + Log3 $name, 4, "$name: got ".$i.' entries from Events (latest: '.FmtDateTime($newlastupdate).')'; + + } + +} + + +sub withings_Get($$@) { my ($hash, $name, $cmd) = @_; my $list; @@ -605,24 +2315,50 @@ withings_Get($$@) $list = "update:noArg updateAll:noArg"; if( $cmd eq "updateAll" ) { - $cmd = "update"; - CommandDeleteReading( undef, "$name .*" ); - } - - if( $cmd eq "update" ) { - withings_poll($hash); + withings_poll($hash,2); return undef; } - } elsif( $hash->{SUBTYPE} eq "DEVICE" ) { - $list = "update:noArg updateAll:noArg"; - - if( $cmd eq "updateAll" ) { - $cmd = "update"; - CommandDeleteReading( undef, "$name .*" ); + elsif( $cmd eq "update" ) { + withings_poll($hash,1); + return undef; } + } elsif( $hash->{SUBTYPE} eq "DEVICE" || $hash->{SUBTYPE} eq "DUMMY" ) { + $list = "update:noArg updateAll:noArg"; + $list .= " videoLink:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '21'); + $list .= " videoCredentials:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '22'); + $list .= " settings:noArg" if(defined($hash->{modelID}) && $hash->{modelID} eq '60' && AttrVal($name,"IP",undef)); - if( $cmd eq "update" ) { - withings_poll($hash); + + if( $cmd eq "videoCredentials" ) { + my $credentials = withings_getS3Credentials($hash); + return undef; + } + elsif( $cmd eq "videoLink" ) { + my $ret = "Flash Player Links:\n"; + my $videolinkdata = withings_getVideoLink($hash); + if(defined($videolinkdata->{body}{device})) + { + #$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}."/"; + #$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}."/"; + $ret .= " Play video from internet (Flash)\n"; + $ret .= " Play video from local network (Flash)\n"; + } + else + { + $ret .= " no links available"; + } + return $ret; + } + elsif( $cmd eq "updateAll" ) { + withings_poll($hash,2); + return undef; + } + elsif( $cmd eq "update" ) { + withings_poll($hash,1); + return undef; + } + elsif( $cmd eq "settings" ) { + withings_readAuraAlarm($hash); return undef; } } elsif( $hash->{SUBTYPE} eq "ACCOUNT" ) { @@ -642,7 +2378,7 @@ withings_Get($$@) my $devices = withings_getDevices($hash); my $ret; foreach my $device (@{$devices}) { - my $detail = withings_getDeviceDetail($hash,$device->{deviceid}); + my $detail = $device->{deviceproperties}; $ret .= "$detail->{id}\t$device_types{$detail->{type}}\t$detail->{batterylvl}\t$detail->{sn}\n"; } @@ -655,26 +2391,543 @@ withings_Get($$@) return "Unknown argument $cmd, choose one of $list"; } -sub -withings_Attr($$$) -{ +sub withings_Set($$@) { + my ( $hash, $name, $cmd, @arg ) = @_; + + my $list=""; + if( $hash->{SUBTYPE} eq "DEVICE" and defined($hash->{modelID}) && $hash->{modelID} eq "60" && AttrVal($name,"IP",undef)) + { + $list = " nap:noArg sleep:noArg alarm:noArg"; + $list .= " stop:noArg snooze:noArg"; + $list .= " nap_volume:slider,0,1,100 nap_brightness:slider,0,1,100"; + $list .= " sleep_volume:slider,0,1,100 sleep_brightness:slider,0,1,100"; + $list .= " clock_state:on,off clock_brightness:slider,0,1,100"; + $list .= " flashMat"; + $list .= " sensors:on,off"; + $list .= " rawCmd"; + if (defined($hash->{helper}{ALARMSCOUNT})&&($hash->{helper}{ALARMSCOUNT}>0)) + { + for(my $i=1;$i<=$hash->{helper}{ALARMSCOUNT};$i++) + { + $list .= " alarm".$i."_time alarm".$i."_volume:slider,0,1,100 alarm".$i."_brightness:slider,0,1,100"; + $list .= " alarm".$i."_state:on,off alarm".$i."_wdays"; + $list .= " alarm".$i."_smartwake:slider,0,1,60"; + } + } + + + 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' ) + { + return withings_setAuraAlarm($hash,$cmd); + } + elsif ( lc $cmd eq 'rawcmd') + { + return withings_setAuraDebug($hash,join( "", @arg )); + + } + #elsif( index( $cmd, "alarm" ) != -1 ) + #{ + # my $alarmno = int( substr( $cmd, 5 ) ) + 0; + # return( withings_parseAlarm( $hash, $alarmno, @arg ) ); + #} + elsif ( lc $cmd =~ /^alarm/ or lc $cmd =~ /^nap/ or lc $cmd =~ /^sleep/ or lc $cmd =~ /^clock/ or lc $cmd eq 'smartwake' ) + { + readingsSingleUpdate( $hash, $cmd, join( ",", @arg ), 1 ); + return withings_setAuraAlarm($hash,$cmd,join( ":", @arg )); + } + elsif ( lc $cmd eq "flashmat" ) + { + return withings_setAuraAlarm($hash,$cmd,join( ":", @arg )); + } + elsif ( lc $cmd eq "sensors" ) + { + return withings_setAuraAlarm($hash,$cmd,join( ":", @arg )); + } + return "Unknown argument $cmd, choose one of $list"; + } + else + { + return "Unknown argument $cmd, choose one of $list"; + } +} + + +sub withings_readAuraAlarm($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name: readauraalarm"; + + my $auraip = AttrVal($name,"IP",undef); + return if(!$auraip); + + my $socket = new IO::Socket::INET ( + PeerHost => $auraip, + PeerPort => '7685', + Proto => 'tcp', + Timeout => 5, + ) or die "ERROR in Socket Creation : $!\n"; + return if(!$socket); + $socket->autoflush(1); + + my $data = "000100010100050101010000"; #hello + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="010100050101110000"; #hello2 + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + + $data="0101000a01090a0005090a000100"; #ping + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + $data="010100050101250000"; #new alarmdata + $socket->send(pack('H*', $data)); + $socket->flush(); + + $socket->recv($data, 1024); + $socket->flush(); + + + + my $datalength = ord(substr($data,2,1))*256 + ord(substr($data,3,1)); + Log3 $name, 5, "$name: alarmdata ($datalength)".unpack('H*', $data); + + my $base = 9; + readingsBeginUpdate($hash); + my $alarmcounter = 1; + + my @dataarray = split("05120007",unpack('H*', $data)); + + while(defined($dataarray[$alarmcounter])) + { + my @alarmparts = split("091600",$dataarray[$alarmcounter]);#seriously, withings? + my $timedatehex = pack('H*', $alarmparts[0]); + + my $alarmhour = ord(substr($timedatehex,0,1)); + my $alarmminute = ord(substr($timedatehex,1,1)); + my $alarmdays = ord(substr($timedatehex,2,1)); + my $alarmstate = (ord(substr($timedatehex,2,1)) > 128) ? "on" : "off"; + my $alarmperiod = ord(substr($timedatehex,6,1)); + + my $alarmvolume = 0; + my $alarmbrightness = 0; + my $alarmsong = 0; + + for(my $i=1;$i<=3;$i++) #whoever did this must have been high as fuck! + { + my $hexdata = pack('H*', $alarmparts[$i]); + my $datatype = ord(substr($hexdata,1,1)); #order is not consistent + my $datalength = ord(substr($hexdata,2,1)); + if($datatype == 1) + { + $alarmvolume = ord(substr($hexdata,3,1))-48; #value as ascii characters + $alarmvolume = $alarmvolume*10 + ord(substr($hexdata,4,1))-48 if($datalength>1); + $alarmvolume = $alarmvolume*10 + ord(substr($hexdata,5,1))-48 if($datalength>2); + } + elsif($datatype == 2) + { + $alarmbrightness = ord(substr($hexdata,3,1))-48; #same for other values - wtf? + $alarmbrightness = $alarmbrightness*10 + ord(substr($hexdata,4,1))-48 if($datalength>1); + $alarmbrightness = $alarmbrightness*10 + ord(substr($hexdata,5,1))-48 if($datalength>2); + } + elsif($datatype == 3) + { + $alarmsong = ord(substr($hexdata,3,1))-48; + } + else{ + Log3 $name, 2, "$name: unknown alarm data type: $datatype"; + } + + } + + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_time", sprintf( "%02d:%02d:%02d",$alarmhour,$alarmminute,0), 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_wdays", withings_int2Weekdays($alarmdays), 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_volume", $alarmvolume, 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_brightness", $alarmbrightness, 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_smartwake", $alarmperiod, 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_state", $alarmstate, 1 ); + readingsBulkUpdate( $hash, "alarm".$alarmcounter."_sound", $alarm_sound{$alarmsong}, 1 ); + + Log3 $name, 4, "$name: alarm $alarmstate $alarmhour:$alarmminute ($alarmperiod) on ".withings_int2Weekdays($alarmdays)." light:$alarmbrightness vol:$alarmvolume [$base]"; + + $hash->{helper}{ALARMSCOUNT} = $alarmcounter; + $alarmcounter++; + + } + + + for(my $i=$alarmcounter;$i<10;$i++) + { + fhem( "deletereading $name alarm".$i."_.*" ); + } + + $data="010100050109100000"; #sensordata + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + my $sensors = (ord(substr($data,19,1))==0)?"on":"off"; + readingsBulkUpdate( $hash, "sensors", $sensors, 1 ); + + + + $data="0101000b0109060006090800020300"; #sleepdata + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + #Log3 $name, 4, "$name: sleepdata ".unpack('H*', $data); + + my $sleepvolume = ord(substr($data,13,1)); + my $sleepbrightness = ord(substr($data,14,1)); + my $sleepsong = ord(substr($data,16,1)); + readingsBulkUpdate( $hash, "sleep_volume", $sleepvolume, 1 ); + readingsBulkUpdate( $hash, "sleep_brightness", $sleepbrightness, 1 ); + readingsBulkUpdate( $hash, "sleep_sound", $sleep_sound{$sleepsong}, 1 ); + + + $data="0101000b0109060006090800020200"; #napdata + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + #Log3 $name, 4, "$name: napdata ".unpack('H*', $data); + + my $napvolume = ord(substr($data,13,1)); + my $napbrightness = ord(substr($data,14,1)); + my $napsong = ord(substr($data,16,1)); + readingsBulkUpdate( $hash, "nap_volume", $napvolume, 1 ); + readingsBulkUpdate( $hash, "nap_brightness", $napbrightness, 1 ); + readingsBulkUpdate( $hash, "nap_sound", $nap_sound{$napsong}, 1 ); + + $data="010100050109100000"; #clock + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + my $clockdisplay = ord(substr($data,13,1)); + my $clockbrightness = ord(substr($data,14,1)); + readingsBulkUpdate( $hash, "clock_state", ($clockdisplay ? "on":"off"), 1 ); + readingsBulkUpdate( $hash, "clock_brightness", $clockbrightness, 1 ); + + #Log3 $name, 4, "$name: clock ".unpack('H*', $data); + + + + $data="010100050109070000"; #state + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data, 1024); + $socket->flush(); + + #Log3 $name, 4, "$name: state ".unpack('H*', $data); + + my $devicestate = ord(substr($data,18,1)); + my $alarmtype = ord(substr($data,13,1)); + + if($devicestate eq 0) + { + readingsBulkUpdate( $hash, "state", "off", 1 ); + } + elsif($devicestate eq 2) + { + readingsBulkUpdate( $hash, "state", "snoozed", 1 ); + } + elsif($devicestate eq 1) + { + readingsBulkUpdate( $hash, "state", "sleep", 1 ) if($alarmtype eq 1); + readingsBulkUpdate( $hash, "state", "alarm", 1 ) if($alarmtype eq 2); + readingsBulkUpdate( $hash, "state", "nap", 1 ) if($alarmtype eq 3); + } + + readingsEndUpdate($hash,1); + + + $socket->close(); + return; + +} + +sub withings_setAuraAlarm($$;$) { + my ($hash, $setting, $value) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name: setaura ".$setting; + + my $auraip = AttrVal($name,"IP",undef); + return if(!$auraip); + + my $socket = new IO::Socket::INET ( + PeerHost => $auraip, + PeerPort => '7685', + Proto => 'tcp', + Timeout => 5, + ) or die "ERROR in Socket Creation : $!\n"; + return if(!$socket); + $socket->autoflush(1); + + my $data = "000100010100050101010000"; #hello + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="010100050101110000"; #hello2 + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="0101000a01090a0005090a000100"; #ping + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="010100050109070000"; #getstate + + if($setting eq "nap") + { + $data="0101000b0109030006090800020200"; #nap + } + elsif($setting eq "sleep") + { + $data="0101000b0109030006090800020300"; #sleep + } + elsif($setting eq "alarm") + { + $data="0101000b0109030006090800020400"; #alarm + } + elsif($setting eq "stop") + { + $data="010100050109040000"; #stop + } + elsif($setting eq "snooze") + { + $data="010100050109110000"; #snooze + } + elsif($setting =~ /^alarm/) + { + my $alarmno = int( substr( $setting,5,1 ) ) + 0; + + my $volume = ReadingsVal( $name, "alarm".$alarmno."_volume", 60); + my $volumestring = ""; + + if($volume > 99) + { + $volumestring = "050103" . sprintf("%.2x",substr($volume,0,1)+48) . sprintf("%.2x",substr($volume,1,1)+48) . sprintf("%.2x",substr($volume,2,1)+48); + } + elsif($volume > 9) + { + $volumestring = "040102" . sprintf("%.2x",substr($volume,0,1)+48) . sprintf("%.2x",substr($volume,1,1)+48); + } + else + { + $volumestring = "030101" . sprintf("%.2x",$volume+48); + } + + my $brightness = ReadingsVal( $name, "alarm".$alarmno."_brightness", 60); + my $brightnessstring = ""; + + if($brightness > 99) + { + $brightnessstring = "050203" . sprintf("%.2x",substr($brightness,0,1)+48) . sprintf("%.2x",substr($brightness,1,1)+48) . sprintf("%.2x",substr($brightness,2,1)+48);; + } + elsif($brightness > 9) + { + $brightnessstring = "040202" . sprintf("%.2x",substr($brightness,0,1)+48) . sprintf("%.2x",substr($brightness,1,1)+48); + } + else + { + $brightnessstring = "030201" . sprintf("%.2x",$brightness+48); + } + + $data = "05120007"; + my @timestr = split(":",ReadingsVal( $name, "alarm".$alarmno."_time", "07:00" )); + $data .= sprintf("%.2x%.2x",$timestr[0],$timestr[1]); + my $alarmint = withings_weekdays2Int(ReadingsVal( $name, "alarm".$alarmno."_wdays", "all")); + $alarmint += 128 if(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on"); + $data .= sprintf("%.2x",$alarmint); + $data .= "000000"; + $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_smartwake", 10)); + $data .= "091600"; + $data .= $volumestring; + $data .= "091600"; + $data .= $brightnessstring; + $data .= "091600030301"; + my $alarmsong = $alarm_song{ReadingsVal( $name, "alarm".$alarmno."_sound", 1)}; + $alarmsong = 1 if(!defined($alarmsong) || $alarmsong==0); + $data .= sprintf("%.2x",$alarmsong+48); + + my $datalen = length($data)/2; + + $data = "010100".sprintf("%.2x",$datalen+10)."01012900".sprintf("%.2x",$datalen+5)."01260001".sprintf("%.2x",$alarmno).$data; + + #if($setting =~ /volume/i or $setting =~ /brightness/i or $setting =~ /song/i ) + #{ + # $data = "0101000d010905000809060004"; + # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_volume", 60)); + # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_brightness", 60)); + # $data .= "04"; + # $data .= sprintf("%.2x",$alarm_song{ReadingsVal( $name, "alarm".$alarmno."_sound", 1)}); + #} + #else + #{ + # $data = "0101001101011b000c09040008"; + # my @timestr = split(":",ReadingsVal( $name, "alarm".$alarmno."_time", "07:00" )); + # $data .= sprintf("%.2x%.2x",$timestr[0],$timestr[1]); + # my $alarmint = withings_weekdays2Int(ReadingsVal( $name, "alarm".$alarmno."_wdays", "all")); + # $alarmint += 128 if(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on"); + # $data .= sprintf("%.2x",$alarmint); + # $data .= "6565d2"; + # $data .= sprintf("%.2x",(ReadingsVal( $name, "alarm".$alarmno."_state", "on") eq "on" ? 1 : 0)); + # $data .= sprintf("%.2x",ReadingsVal( $name, "alarm".$alarmno."_smartwake", 10)); + #} + Log3 $name, 5, "$name: set alarm ".$data; + + } + elsif($setting =~ /^nap/) + { + $data = "0101000d010905000809060004"; + $data .= sprintf("%.2x",ReadingsVal( $name, "nap_volume", 25)); + $data .= sprintf("%.2x",ReadingsVal( $name, "nap_brightness", 25)); + $data .= "02"; + $data .= sprintf("%.2x",ReadingsVal( $name, "nap_sound", 1)==0?1:ReadingsVal( $name, "nap_sound", 1)); + } + elsif($setting =~ /^sleep/) + { + $data = "0101000d010905000809060004"; + $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_volume", 25)); + $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_brightness", 10)); + $data .= "03"; + $data .= sprintf("%.2x",ReadingsVal( $name, "sleep_sound", 1)==0?1:ReadingsVal( $name, "sleep_sound", 1)); + } + elsif($setting =~ /^clock/) + { + $data = "0101000b01090f0006090d0002"; + $data .= (ReadingsVal( $name, "clock_state", 1) ? "01":"00"); + $data .= sprintf("%.2x",ReadingsVal( $name, "clock_brightness", 40)); + } + + elsif($setting =~ /^flashMat/) + { + $data = "0101003201090c002d0413001211"; + $data .= unpack('H*', $value); + $data .= "080a0013000000000000000000004e200000000000ffff"; + } + + elsif($setting =~ /^sensors/) + { + $data = "0101000a01090f0005080b000100"; + $data = "0101000a01090f0005080b000101" if($value eq "off"); + } + + Log3 $name, 3, "$name: writesocket ".$data; + + + + $socket->send(pack('H*', $data)); + $socket->flush(); + + $socket->recv($data, 1024); + $socket->flush(); + + Log3 $name, 4, "$name: readsocket ".unpack('H*', $data); + + $socket->close(); + return; + +} + +sub withings_setAuraDebug($$;$) { + my ($hash, $value) = @_; + my $name = $hash->{NAME}; + + my $auraip = AttrVal($name,"IP",undef); + return if(!$auraip); + + my $socket = new IO::Socket::INET ( + PeerHost => $auraip, + PeerPort => '7685', + Proto => 'tcp', + Timeout => 5, + ) or die "ERROR in Socket Creation : $!\n"; + return if(!$socket); + $socket->autoflush(1); + + my $data = "000100010100050101010000"; #hello + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="010100050101110000"; #hello2 + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data="0101000a01090a0005090a000100"; #ping + $socket->send(pack('H*', $data)); + $socket->flush(); + $socket->recv($data,1024); + $socket->flush(); + + $data=$value; #debug + Log3 $name, 5, "$name: writesocket ".$data; + Log3 $name, 5, "$name: writesocket ".pack('H*', $data); + + $socket->send(pack('H*', $data)); + $socket->flush(); + $data=""; + $socket->recv($data, 1024); + $socket->flush(); + + Log3 $name, 5, "$name: readsocket ".$data; + Log3 $name, 5, "$name: readsocket ".unpack('H*', $data); + + $socket->close(); + return; + +} + +sub withings_Attr($$$) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; - $attrVal = int($attrVal) if($attrName eq "interval"); - $attrVal = 3600 if($attrName eq "interval" && $attrVal < 3600 && $attrVal != 0); + $attrVal = int($attrVal) if($attrName eq "intervalData" or $attrName eq "intervalAlert" or $attrName eq "intervalProperties" or $attrName eq "intervalDebug"); + $attrVal = 300 if($attrName eq "intervalData" && $attrVal < 300 ); + $attrVal = 300 if($attrName eq "intervalDebug" && $attrVal < 300 ); + $attrVal = 120 if($attrName eq "intervalAlert" && $attrVal < 120 ); + $attrVal = 1800 if($attrName eq "intervalProperties" && $attrVal < 1800 ); - if( $attrName eq "interval" ) { - my $hash = $defs{$name}; - $hash->{INTERVAL} = $attrVal; - $hash->{INTERVAL} = 3600 if( !$attrVal ); - } elsif( $attrName eq "disable" ) { + if( $attrName eq "disable" ) { my $hash = $defs{$name}; RemoveInternalTimer($hash); if( $cmd eq "set" && $attrVal ne "0" ) { } else { $attr{$name}{$attrName} = 0; - withings_poll($hash); + withings_poll($hash,0); + } + } + elsif( $attrName eq "nossl" ) { + my $hash = $defs{$name}; + if( $cmd eq "set" && $attrVal ne "0" ) { + $hash->{'.https'} = "http"; + } else { + $hash->{'.https'} = "https"; } } @@ -688,10 +2941,368 @@ withings_Attr($$$) return; } +########################## + +sub withings_Dispatch($$$) { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + Log3 $name, 5, "$name: dispatch ".$param->{type}; + + + if( $err ) + { + Log3 $name, 1, "$name: http request failed: type $param->{type} - $err"; + } + elsif( $data ) + { + + $data =~ s/\n//g; + if( $data !~ /{.*}/ or $data =~ />$data<< " . $param->{type} if($data ne "[]"); + return undef; + } + + my $json; + $json = JSON->new->utf8(0)->decode($data); + + Log3 $name, 4, "$name: json returned: ".Dumper($json); + + if(defined($param->{enddate})) + { + $json->{requestedenddate} = $param->{enddate}; + } + + if( $param->{type} eq 'deviceReadingsScale' || $param->{type} eq 'deviceReadingsBedside' || $param->{type} eq 'deviceReadingsHome' ) { + withings_parseMeasurements($hash, $json); + } elsif( $param->{type} eq 'userReadingsSleep' || $param->{type} eq 'userReadingsSleepDebug' || $param->{type} eq 'userReadingsActivity' ) { + withings_parseVasistas($hash, $json, $param->{type}); + } elsif( $param->{type} eq 'deviceReadingsBaby' || $param->{type} eq 'deviceAlertsBaby' ) { + withings_parseEvents($hash, $json); + } elsif( $param->{type} eq 'deviceAlertsHome' ) { + withings_parseTimeline($hash, $json); + } elsif( $param->{type} eq 'userReadingsCommon' ) { + withings_parseMeasureGroups($hash, $json); + } elsif( $param->{type} eq 'userDailyAggregate' ) { + withings_parseAggregate($hash, $json); + } elsif( $param->{type} eq 'userDailyActivity' ) { + withings_parseActivity($hash, $json); + } elsif( $param->{type} eq 'userDailyWorkouts' ) { + withings_parseWorkouts($hash, $json); + } elsif( $param->{type} eq 'deviceProperties' ) { + withings_parseProperties($hash, $json); + } + } +} + + + + +########################## +sub withings_DbLog_splitFn($) { + my ($event) = @_; + my ($reading, $value, $unit) = ""; + + Log3 ("dbsplit", 5, "withings dbsplit event ".$event); + + my @parts = split(/ /,$event,3); + $reading = $parts[0]; + $reading =~ tr/://d; + $value = $parts[1]; + + + if($event =~ m/heartPulse/) + { + $reading = 'heartPulse'; + $unit = 'bpm'; + } + elsif($event =~ m/pulseWave/) + { + $reading = 'pulseWave'; + $unit = 'm/s'; + } + elsif($event =~ m/dailyDescent/) + { + $reading = 'dailyDescent'; + $unit = 'm'; + } + elsif($event =~ m/dailyDistance/) + { + $reading = 'dailyDistance'; + $unit = 'm'; + } + elsif($event =~ m/dailyElevation/) + { + $reading = 'dailyElevation'; + $unit = 'm'; + } + elsif($event =~ m/dailySteps/) + { + $reading = 'dailySteps'; + $unit = 'steps'; + } + elsif($event =~ m/steps/) + { + $reading = 'steps'; + $unit = 'steps'; + } + elsif($event =~ m/temperature/) + { + $reading = 'temperature'; + $unit = '˚C'; + } + elsif($event =~ m/bodyTemperature/) + { + $reading = 'bodyTemperature'; + $unit = '˚C'; + } + elsif($event =~ m/skinTemperature/) + { + $reading = 'skinTemperature'; + $unit = '˚C'; + } + elsif($event =~ m/humidity/) + { + $reading = 'humidity'; + $unit = '%'; + } + elsif($event =~ m/systolicBloodPressure/) + { + $reading = 'systolicBloodPressure'; + $unit = 'mmHg'; + } + elsif($event =~ m/diastolicBloodPressure/) + { + $reading = 'diastolicBloodPressure'; + $unit = 'mmHg'; + } + elsif($event =~ m/spo2/) + { + $reading = 'spo2'; + $unit = '%'; + } + elsif($event =~ m/boneMassWeight/) + { + $reading = 'boneMassWeight'; + $unit = 'kg'; + } + elsif($event =~ m/fatFreeMass/) + { + $reading = 'fatFreeMass'; + $unit = 'kg'; + } + elsif($event =~ m/fatMassWeight/) + { + $reading = 'fatMassWeight'; + $unit = 'kg'; + } + elsif($event =~ m/weight/) + { + $reading = 'weight'; + $unit = 'kg'; + } + elsif($event =~ m/muscleRatio/) + { + $reading = 'muscleRatio'; + $unit = '%'; + } + elsif($event =~ m/boneRatio/) + { + $reading = 'boneRatio'; + $unit = '%'; + } + elsif($event =~ m/fatRatio/) + { + $reading = 'fatRatio'; + $unit = '%'; + } + elsif($event =~ m/hydration/) + { + $reading = 'hydration'; + $unit = '%'; + } + elsif($event =~ m/waterMass/) + { + $reading = 'waterMass'; + $unit = 'kg'; + } + elsif($event =~ m/dailyCaloriesPassive/) + { + $reading = 'dailyCaloriesPassive'; + $unit = 'kcal'; + } + elsif($event =~ m/dailyCaloriesActive/) + { + $reading = 'dailyCaloriesActive'; + $unit = 'kcal'; + } + elsif($event =~ m/calories/) + { + $reading = 'calories'; + $unit = 'kcal'; + } + elsif($event =~ m/co2/) + { + $reading = 'co2'; + $unit = 'ppm'; + } + elsif($event =~ m/voc/) + { + $reading = 'voc'; + $unit = 'ppm'; + } + elsif($event =~ m/light/) + { + $reading = 'light'; + $unit = 'lux'; + } + elsif($event =~ m/batteryLevel/) + { + $reading = 'batteryLevel'; + $unit = '%'; + } + else + { + $value = $parts[1]; + $value = $value." ".$parts[2] if(defined($parts[2])); + } + #Log3 ("dbsplit", 5, "withings dbsplit output ".$reading." / ".$value." / ".$unit); + + return ($reading, $value, $unit); +} + +sub withings_int2Weekdays( $ ) { + my ($wdayint) = @_; + my $wdayargs = ''; + my $weekdays = ''; + + $wdayint -= 128 if($wdayint >= 128); + + if($wdayint >= 64) + { + $wdayargs.="Sa"; + $wdayint-=64; + } + if($wdayint >= 32) + { + $wdayargs.="Fr"; + $wdayint-=32; + } + if($wdayint >= 16) + { + $wdayargs.="Th"; + $wdayint-=16; + } + if($wdayint >= 8) + { + $wdayargs.="We"; + $wdayint-=8; + } + if($wdayint >= 4) + { + $wdayargs.="Tu"; + $wdayint-=4; + } + if($wdayint >= 2) + { + $wdayargs.="Mo"; + $wdayint-=2; + } + if($wdayint >= 1) + { + $wdayargs.="Su"; + $wdayint-=1; + } + + if(index($wdayargs,"Mo") != -1) + { + $weekdays.='Mo,'; + } + if(index($wdayargs,"Tu") != -1) + { + $weekdays.='Tu,'; + } + if(index($wdayargs,"We") != -1) + { + $weekdays.='We,'; + } + if(index($wdayargs,"Th") != -1) + { + $weekdays.='Th,'; + } + if(index($wdayargs,"Fr") != -1) + { + $weekdays.='Fr,'; + } + if(index($wdayargs,"Sa") != -1) + { + $weekdays.='Sa,'; + } + if(index($wdayargs,"Su") != -1) + { + $weekdays.='Su,'; + } + if($weekdays eq "Mo,Tu,We,Th,Fr,Sa,Su,") + { + $weekdays="all"; + } + if($weekdays eq "") + { + $weekdays="none"; + } + $weekdays=~ s/,$//; + return $weekdays; +} + +sub withings_weekdays2Int( $ ) { + my ($wdayargs) = @_; + my $weekdays = 0; + if(index($wdayargs,"Mo") != -1 || index($wdayargs,"1") != -1) + { + $weekdays+=2; + } + if(index($wdayargs,"Tu") != -1 || index($wdayargs,"Di") != -1 || index($wdayargs,"2") != -1) + { + $weekdays+=4; + } + if(index($wdayargs,"We") != -1 || index($wdayargs,"Mi") != -1 || index($wdayargs,"3") != -1) + { + $weekdays+=8; + } + if(index($wdayargs,"Th") != -1 || index($wdayargs,"Do") != -1 || index($wdayargs,"4") != -1) + { + $weekdays+=16; + } + if(index($wdayargs,"Fr") != -1 || index($wdayargs,"5") != -1) + { + $weekdays+=32; + } + if(index($wdayargs,"Sa") != -1 || index($wdayargs,"6") != -1) + { + $weekdays+=64; + } + if(index($wdayargs,"Su") != -1 || index($wdayargs,"So") != -1 || index($wdayargs,"0") != -1) + { + $weekdays+=1; + } + if(index($wdayargs,"all") != -1 || index($wdayargs,"daily") != -1 || index($wdayargs,"7") != -1) + { + $weekdays=127; + } + if(index($wdayargs,"none") != -1 || index($wdayargs,"once") != -1) + { + $weekdays=0; + } + return $weekdays; +} 1; =pod +=item device +=item summary Withings health data for users and devices =begin html @@ -707,9 +3318,8 @@ withings_Attr($$$) Define
diff --git a/fhem/FHEM/38_netatmo.pm b/fhem/FHEM/38_netatmo.pm index 0ef9afc1c..4335d4484 100644 --- a/fhem/FHEM/38_netatmo.pm +++ b/fhem/FHEM/38_netatmo.pm @@ -10,7 +10,7 @@ # # ############################################################################## -# Release 05 / 2017-01-15 +# Release 06 / 2017-02-19 package main; @@ -352,6 +352,7 @@ netatmo_Define($$) } elsif( @a == 6 || ($a[2] eq "ACCOUNT" && @a == 7 ) ) { $subtype = "ACCOUNT"; + $hash->{network} = "ok"; my $user = $a[@a-4]; my $pass = $a[@a-3]; @@ -439,7 +440,7 @@ sub netatmo_InitWait($) { } else { - InternalTimer(gettimeofday()+90, "netatmo_InitWait", $hash, 0); + InternalTimer(gettimeofday()+120, "netatmo_InitWait", $hash, 0); } return undef; @@ -666,10 +667,8 @@ netatmo_refreshToken($;$) { my ($hash,$nonblocking) = @_; - if( !$hash->{access_token} ) { - netatmo_getToken($hash); - return undef; - } elsif( !$nonblocking && defined($hash->{expires_at}) ) { + + if( $hash->{access_token} && !$nonblocking && defined($hash->{expires_at}) ) { my ($seconds) = gettimeofday(); return undef if( $seconds < $hash->{expires_at} - 300 ); } @@ -678,15 +677,24 @@ netatmo_refreshToken($;$) if(!defined($resolve)) { $hash->{STATE} = "DNS error"; + $hash->{network} = "dns" if($hash->{SUBTYPE} eq "ACCOUNT"); delete($hash->{access_token}); + delete($hash->{access_token_app}); InternalTimer( gettimeofday() + 1800, "netatmo_refreshTokenTimer", $hash, 0); return undef; } + if( !$hash->{access_token} ) { + netatmo_getToken($hash); + return undef; + } + + + if( $nonblocking ) { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/oauth2/token", - timeout => 10, + timeout => 20, noshutdown => 1, data => {grant_type => 'refresh_token', client_id => $hash->{helper}{client_id}, client_secret=> $hash->{helper}{client_secret}, refresh_token => $hash->{refresh_token}}, hash => $hash, @@ -711,6 +719,12 @@ netatmo_refreshAppToken($;$) my ($hash,$nonblocking) = @_; my $name = $hash->{NAME}; + if($hash->{network} eq "dns") + { + InternalTimer( gettimeofday() + 1800, "netatmo_refreshAppTokenTimer", $hash, 0); + return undef; + } + if( !$hash->{access_token_app} ) { Log3 $name, 2, "$name: missing app token!"; @@ -721,14 +735,7 @@ netatmo_refreshAppToken($;$) return undef if( $seconds < $hash->{expires_at_app} - 300 ); } - my $resolve = inet_aton("api.netatmo.com"); - if(!defined($resolve)) - { - $hash->{STATE} = "DNS error"; - InternalTimer( gettimeofday() + 1800, "netatmo_refreshAppTokenTimer", $hash, 0); - delete($hash->{access_token_app}); - return undef; - } + Log3 $name, 4, "$name: refreshing app token"; my $auth = "QXV0aG9yaXphdGlvbjogQmFzaWMgYm1GZlkyeHBaVzUwWDJsdmN6bzFObU5qTmpSaU56azBOak5oT1RrMU9HSTNOREF4TkRjeVpEbGxNREUxT0E9PQ=="; $auth = decode_base64($auth); @@ -736,7 +743,7 @@ netatmo_refreshAppToken($;$) if( $nonblocking ) { HttpUtils_NonblockingGet({ url => "https://app.netatmo.net/oauth2/token", - timeout => 10, + timeout => 20, noshutdown => 1, header => "$auth", data => {grant_type => 'refresh_token', refresh_token => $hash->{refresh_token_app}}, @@ -787,7 +794,7 @@ netatmo_connect($) netatmo_getToken($hash); #netatmo_getAppToken($hash); - InternalTimer(gettimeofday()+60, "netatmo_poll", $hash, 0); + InternalTimer(gettimeofday()+90, "netatmo_poll", $hash, 0); } sub @@ -874,7 +881,7 @@ netatmo_initDevice($) return undef if(AttrVal($name,"disable",0) eq "1"); - InternalTimer(gettimeofday()+60, "netatmo_poll", $hash, 0); + InternalTimer(gettimeofday()+90, "netatmo_poll", $hash, 0); #netatmo_poll($hash); } @@ -891,6 +898,7 @@ netatmo_getDevices($;$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://api.netatmo.com/api/getstationsdata", + timeout => 5, noshutdown => 1, data => { access_token => $hash->{access_token}, }, }); @@ -901,6 +909,7 @@ netatmo_getDevices($;$) } else { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getstationsdata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -923,6 +932,7 @@ netatmo_getHomes($;$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://api.netatmo.com/api/gethomedata", + timeout => 5, noshutdown => 1, data => { access_token => $hash->{access_token}, }, }); @@ -932,6 +942,7 @@ netatmo_getHomes($;$) } else { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/gethomedata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -952,6 +963,7 @@ netatmo_getThermostats($;$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://api.netatmo.com/api/getthermostatsdata", + timeout => 5, noshutdown => 1, data => { access_token => $hash->{access_token}, }, }); @@ -962,6 +974,7 @@ netatmo_getThermostats($;$) } else { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getthermostatsdata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -985,6 +998,7 @@ netatmo_getHomecoachs($;$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://api.netatmo.com/api/gethomecoachsdata", + timeout => 5, noshutdown => 1, data => { access_token => $hash->{access_token}, }, }); @@ -995,6 +1009,7 @@ netatmo_getHomecoachs($;$) } else { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/gethomecoachsdata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -1023,7 +1038,8 @@ netatmo_pingCamera($;$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => $pingurl, - noshutdown => 1, + timeout => 10, + sslargs => { SSL_hostname => '', }, data => { access_token => $iohash->{access_token}, }, }); netatmo_dispatch( {hash=>$hash,type=>'cameraping'},$err,$data ); @@ -1033,7 +1049,8 @@ netatmo_pingCamera($;$) } else { HttpUtils_NonblockingGet({ url => $pingurl, - noshutdown => 1, + timeout => 10, + sslargs => { SSL_hostname => '', }, data => { access_token => $iohash->{access_token}, }, hash => $hash, type => 'cameraping', @@ -1171,6 +1188,7 @@ netatmo_getEvents($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getnextevents", + timeout => 20, noshutdown => 1, data => { access_token => $iohash->{access_token}, home_id => $hash->{Home}, event_id => $hash->{lastevent}, }, hash => $hash, @@ -1223,6 +1241,7 @@ netatmo_getPublicDevices($$;$$$$) if( $blocking ) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://api.netatmo.com/api/getpublicdata", + timeout => 5, noshutdown => 1, data => { access_token => $iohash->{access_token}, lat_ne => $lat_ne, lon_ne => $lon_ne, lat_sw => $lat_sw, lon_sw => $lon_sw }, }); @@ -1231,6 +1250,7 @@ netatmo_getPublicDevices($$;$$$$) } else { HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getpublicdata", + timeout => 20, noshutdown => 1, data => { access_token => $iohash->{access_token}, lat_ne => $lat_ne, lon_ne => $lon_ne, lat_sw => $lat_sw, lon_sw => $lon_sw, filter => 'true' }, hash => $hash, @@ -1338,7 +1358,7 @@ netatmo_requestDeviceReadings($@) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getmeasure", - timeout => 10, + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1368,7 +1388,7 @@ netatmo_initHome($@) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/gethomedata", - timeout => 10, + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1400,7 +1420,7 @@ netatmo_requestHomeReadings($@) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/gethomedata", - timeout => 10, + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1430,7 +1450,7 @@ netatmo_requestThermostatReadings($@) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getthermostatsdata", - timeout => 10, + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1459,7 +1479,7 @@ netatmo_requestPersonReadings($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getlasteventof", - timeout => 10, + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1504,7 +1524,7 @@ netatmo_setPresence($$) HttpUtils_NonblockingGet({ url => "https://app.netatmo.net/api/setpersons".$urlstatus, - timeout => 10, + timeout => 20, noshutdown => 1, method => "POST", header => "Content-Type: application/json\r\nAuthorization: Bearer ".$iohash->{access_token_app}, @@ -1540,6 +1560,7 @@ netatmo_setCamera($$$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'camerastatus', callback => \&netatmo_dispatch, @@ -1570,6 +1591,7 @@ netatmo_setCameraSetting($$$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'camerastatus', callback => \&netatmo_dispatch, @@ -1600,6 +1622,7 @@ netatmo_setFloodlight($$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'camerastatus', callback => \&netatmo_dispatch, @@ -1630,6 +1653,7 @@ netatmo_setIntensity($$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'camerastatus', callback => \&netatmo_dispatch, @@ -1661,6 +1685,7 @@ netatmo_setPresenceConfig($$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'camerastatus', callback => \&netatmo_dispatch, @@ -1691,6 +1716,7 @@ netatmo_getPresenceConfig($) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'cameraconfig', callback => \&netatmo_dispatch, @@ -1727,6 +1753,7 @@ netatmo_setTagCalibration($$) HttpUtils_NonblockingGet({ url => $commandurl, noshutdown => 1, + verify_hostname => 0, hash => $hash, type => 'tagstatus', callback => \&netatmo_dispatch, @@ -1761,6 +1788,7 @@ netatmo_setThermostatMode($$;$$) HttpUtils_NonblockingGet({ url => 'https://api.netatmo.com/api/setthermpoint', + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1791,6 +1819,7 @@ netatmo_setThermostatTemp($$;$$) HttpUtils_NonblockingGet({ url => 'https://api.netatmo.com/api/setthermpoint', + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1824,6 +1853,7 @@ netatmo_setThermostatProgram($$) HttpUtils_NonblockingGet({ url => 'https://api.netatmo.com/api/switchschedule', + timeout => 20, noshutdown => 1, data => \%data, hash => $hash, @@ -1850,11 +1880,32 @@ netatmo_poll($) # InternalTimer( gettimeofday() + 1800, "netatmo_poll", $hash, 0); # return undef; # } - + $hash->{INTERVAL} = 3600 if(!defined($hash->{INTERVAL})); + + if(defined($hash->{status}) && $hash->{status} =~ /usage/) { RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{INTERVAL}+1800, "netatmo_poll", $hash, 0); Log3 $name, 1, "$name: API usage limit reached"; + $hash->{status} = "postponed update"; + return undef; + } + + $hash->{status} = "ok"; + + if( $hash->{SUBTYPE} eq "ACCOUNT" && defined($hash->{network}) && $hash->{network} eq "timeout" ) { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+120, "netatmo_poll", $hash, 0); + $hash->{status} = "recovering network"; + return undef; + } elsif( $hash->{SUBTYPE} eq "ACCOUNT" && defined($hash->{network}) && $hash->{network} ne "ok" ) { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "netatmo_poll", $hash, 0); + $hash->{status} = "recovering network"; + return undef; + } elsif( $hash->{SUBTYPE} ne "ACCOUNT" && defined($hash->{IODev}->{network}) && $hash->{IODev}->{network} ne "ok" ) { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{INTERVAL}, "netatmo_poll", $hash, 0); $hash->{status} = "delayed update"; return undef; } @@ -1885,6 +1936,7 @@ netatmo_poll($) } elsif( $hash->{SUBTYPE} eq "PERSON" ) { netatmo_pollPerson($hash); } else { + Log3 $name, 1, "$name: unknown netatmo type $hash->{SUBTYPE} "; return undef; } @@ -1915,26 +1967,48 @@ netatmo_dispatch($$$) Log3 $name, 2, "$name: http request failed: $err"; if($err =~ /refused/ ){ RemoveInternalTimer($hash); - InternalTimer(gettimeofday()+3600, "netatmo_poll", $hash, 0); + InternalTimer(gettimeofday()+7200, "netatmo_poll", $hash, 0); Log3 $name, 2, "$name: Possible IP Ban by Netatmo servers, try to change your IP and increase your request interval"; $hash->{status} = "banned"; + $hash->{network} = "banned" if($hash->{SUBTYPE} eq "ACCOUNT"); + } + elsif($err =~ /Bad hostname/){ + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+1800, "netatmo_poll", $hash, 0); + $hash->{status} = "timeout"; + $hash->{network} = "disconnected" if($hash->{SUBTYPE} eq "ACCOUNT"); } elsif($err =~ /timed out/){ + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+600, "netatmo_poll", $hash, 0); + $hash->{status} = "timeout"; + $hash->{network} = "timeout" if($hash->{SUBTYPE} eq "ACCOUNT"); + } + elsif($err =~ /Can't connect/){ RemoveInternalTimer($hash); InternalTimer(gettimeofday()+900, "netatmo_poll", $hash, 0); $hash->{status} = "timeout"; + $hash->{network} = "disconnected" if($hash->{SUBTYPE} eq "ACCOUNT"); + #CommandDeleteReading( undef, "$hash->{NAME} vpn_url" ) if($hash->{SUBTYPE} eq "CAMERA"); } + return undef; } elsif( $data ) { $data =~ s/\n//g; if( $data !~ m/^{.*}$/ ) { - Log3 $name, 2, "$name: invalid json detected: \n$data"; - #readingsSingleUpdate( $hash, "active", "error", 1 ); - $hash->{status} = "error"; + RemoveInternalTimer($hash); InternalTimer(gettimeofday()+300, "netatmo_poll", $hash, 0); + Log3 $name, 2, "$name: invalid json detected"; + Log3 $name, 5, "$name: $data"; + $hash->{status} = "error"; + $hash->{network} = "ok" if($hash->{SUBTYPE} eq "ACCOUNT"); + $hash->{IODev}->{network} = "ok" if($hash->{SUBTYPE} ne "ACCOUNT"); return undef; } + $hash->{network} = "ok" if($hash->{SUBTYPE} eq "ACCOUNT"); + $hash->{IODev}->{network} = "ok" if($hash->{SUBTYPE} ne "ACCOUNT"); + my $json; $json = JSON->new->utf8(0)->decode($data); @@ -1943,12 +2017,19 @@ netatmo_dispatch($$$) Log3 $name, 6, Dumper($json); if( $json->{error} ) { + if($json->{error} =~ /invalid_grant/) + { + $hash->{status} = "invalid grant"; + $hash->{STATE} = "LOGIN FAILED"; + return undef; + } + $hash->{status} = $json->{error}{message}; InternalTimer(gettimeofday()+1800, "netatmo_poll", $hash, 0) if($hash->{status} =~ /usage/); return undef if($hash->{status} =~ /usage/); } - + if( $param->{type} eq 'token' ) { netatmo_parseToken($hash,$json); @@ -2326,6 +2407,7 @@ sub netatmo_parseToken($$) { my($hash, $json) = @_; + my $name = $hash->{NAME}; my $had_token = $hash->{access_token}; @@ -2340,10 +2422,11 @@ netatmo_parseToken($$) netatmo_getDevices($hash) if( !$had_token ); - InternalTimer(gettimeofday()+$json->{expires_in}*3/4, "netatmo_refreshTokenTimer", $hash, 0); + InternalTimer(gettimeofday()+$json->{expires_in}*0.95, "netatmo_refreshTokenTimer", $hash, 0); } else { $hash->{STATE} = "Error" if( !$hash->{access_token} ); - InternalTimer(gettimeofday()+300, "netatmo_refreshTokenTimer", $hash, 0); + Log3 $name, 1, "$name: token error ".Dumper($json); + InternalTimer(gettimeofday()+900, "netatmo_refreshTokenTimer", $hash, 0); } } @@ -2361,11 +2444,11 @@ netatmo_parseAppToken($$) ($hash->{expires_at_app}) = gettimeofday(); $hash->{expires_at_app} += $json->{expires_in}; - InternalTimer(gettimeofday()+$json->{expires_in}*3/4, "netatmo_refreshAppTokenTimer", $hash, 0); + InternalTimer(gettimeofday()+$json->{expires_in}*0.95, "netatmo_refreshAppTokenTimer", $hash, 0); } else { $hash->{STATE} = "Error" if( !$hash->{access_token_app} ); Log3 $name, 1, "$name: app token error ".Dumper($json); - InternalTimer(gettimeofday()+300, "netatmo_refreshAppTokenTimer", $hash, 0); + InternalTimer(gettimeofday()+900, "netatmo_refreshAppTokenTimer", $hash, 0); } } @@ -3225,27 +3308,27 @@ netatmo_parseHomeReadings($$;$) { my $eventmessage = $eventdata->{message}; $eventmessage = "-" if(!defined($eventdata->{message})); - $eventmessage =~ s/<\/b>//g; - $eventmessage =~ s///g; + $eventmessage =~ s/<\/b>//g; + $eventmessage =~ s///g; - if(defined($eventdata->{message})) - { - readingsBeginUpdate($hash); - $hash->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $hash, "event", encode_utf8($eventmessage), 1 ); - $hash->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($hash,1); - } + if(defined($eventdata->{message})) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $hash, "event", encode_utf8($eventmessage), 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($hash,1); + } - if(defined($eventdata->{snapshot})) - { - readingsBeginUpdate($hash); - $hash->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $hash, "last_snapshot", "https://api.netatmo.com/api/getcamerapicture?image_id=".$eventdata->{snapshot}{id}."&key=".$eventdata->{snapshot}{key}, 1 ); - $hash->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($hash,1); - } + if(defined($eventdata->{snapshot})) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $hash, "last_snapshot", "https://api.netatmo.com/api/getcamerapicture?image_id=".$eventdata->{snapshot}{id}."&key=".$eventdata->{snapshot}{key}, 1 ); + $hash->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($hash,1); + } } my $camera = $modules{$hash->{TYPE}}{defptr}{"C$eventdata->{camera_id}"}; my $tag = $modules{$hash->{TYPE}}{defptr}{"G$eventdata->{module_id}"} if(defined($eventdata->{module_id})); @@ -3347,62 +3430,62 @@ netatmo_parseHomeReadings($$;$) readingsEndUpdate($camera,1); } - if(defined($eventdata->{time})) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $camera, "event_time", FmtDateTime($eventdata->{time}), 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); - } + if(defined($eventdata->{time})) + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $camera, "event_time", FmtDateTime($eventdata->{time}), 1 ); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } - if(defined($eventdata->{type})) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $camera, "event_type", $eventdata->{type}, 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); - } + if(defined($eventdata->{type})) + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $camera, "event_type", $eventdata->{type}, 1 ); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } - if(defined($eventdata->{id})) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $camera, "event_id", $eventdata->{id}, 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); - } + if(defined($eventdata->{id})) + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $camera, "event_id", $eventdata->{id}, 1 ); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } - if(defined($person)) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $camera, "person_seen", ReadingsVal($person->{NAME},"pseudo","Unknown"), 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); - } + if(defined($person)) + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $camera, "person_seen", ReadingsVal($person->{NAME},"pseudo","Unknown"), 1 ); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } - if(defined($eventdata->{snapshot})) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); - readingsBulkUpdate( $camera, "snapshot", $eventdata->{snapshot}{id}."|".$eventdata->{snapshot}{key}, 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); - } + if(defined($eventdata->{snapshot})) + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + readingsBulkUpdate( $camera, "snapshot", $eventdata->{snapshot}{id}."|".$eventdata->{snapshot}{key}, 1 ); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } if(defined($eventdata->{snapshot})) - { - readingsBeginUpdate($camera); - $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); + { + readingsBeginUpdate($camera); + $camera->{".updateTimestamp"} = FmtDateTime($eventdata->{time}); readingsBulkUpdate( $camera, "last_snapshot", "https://api.netatmo.com/api/getcamerapicture?image_id=".$eventdata->{snapshot}{id}."&key=".$eventdata->{snapshot}{key}, 1 ); - $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); - readingsEndUpdate($camera,1); + $camera->{CHANGETIME}[0] = FmtDateTime($eventdata->{time}); + readingsEndUpdate($camera,1); + } } - } - + if(defined($eventdata->{video_status})) { readingsBeginUpdate($camera); @@ -3586,10 +3669,6 @@ netatmo_parseHomeReadings($$;$) } - else - { - $hash->{STATE} = "Error"; - } } else { @@ -3644,7 +3723,7 @@ netatmo_parseCameraStatus($$;$) InternalTimer( gettimeofday() + 10, "netatmo_pollHome", $home, 0) if($hash->{status} eq "ok" ); } else{ - netatmo_pollHome($home) if($home->{status} !~ /usage/ && $home->{status} !~ /delay/); + netatmo_pollHome($home) if($home->{status} !~ /usage/ && $home->{status} !~ /postponed/); } #readingsSingleUpdate( $hash, "active", $hash->{status}, 1 ); @@ -3677,7 +3756,7 @@ netatmo_parseCameraConfig($$;$) InternalTimer( gettimeofday() + 10, "netatmo_pollHome", $home, 0) if($hash->{status} eq "ok" ); } else{ - netatmo_pollHome($home) if($home->{status} !~ /usage/ && $home->{status} !~ /delay/); + netatmo_pollHome($home) if($home->{status} !~ /usage/ && $home->{status} !~ /postponed/); } #readingsSingleUpdate( $hash, "active", $hash->{status}, 1 ); @@ -3720,6 +3799,8 @@ netatmo_parseCameraVideo($$;$) $hash->{status} = $json->{error}{message} if( $json->{error} ); return undef if($hash->{status} ne "ok"); + return undef if($hash->{status} ne "ok"); + my $lastupdate = ReadingsVal( $name, ".lastupdate", 0 ); readingsSingleUpdate($hash, "local_url", $json->{local_url}, 1) if(defined($json->{local_url})); @@ -3840,10 +3921,6 @@ netatmo_parsePersonReadings($$;$) } - else - { - $hash->{STATE} = "Error"; - } } else { @@ -3880,7 +3957,7 @@ netatmo_parseThermostatReadings($$;$) my $hash = $modules{$hash->{TYPE}}{defptr}{"R$devicedata->{_id}"}; next if (!defined($hash)); next if($devicedata->{_id} ne $hash->{Relay}); - $hash->{STATE} = "Connected"; + #$hash->{STATE} = "Connected"; readingsSingleUpdate($hash, "name", encode_utf8($devicedata->{station_name}), 1) if(defined($devicedata->{station_name})); @@ -4023,10 +4100,6 @@ netatmo_parseThermostatReadings($$;$) } - else - { - $hash->{STATE} = "Error"; - } } else { @@ -4049,10 +4122,10 @@ netatmo_parseThermostatStatus($$;$) if( $json ) { $hash->{status} = $json->{status}; $hash->{status} = $json->{error}{message} if( $json->{error} ); - InternalTimer( gettimeofday() + 15, "netatmo_pollRelay", $thermostat, 0) if($hash->{status} eq "ok"); + InternalTimer( gettimeofday() + 10, "netatmo_pollRelay", $thermostat, 0) if($hash->{status} eq "ok"); } else{ - netatmo_pollRelay($thermostat) if($thermostat->{status} !~ /usage/ && $thermostat->{status} !~ /delay/);; + netatmo_pollRelay($thermostat) if($thermostat->{status} !~ /usage/ && $thermostat->{status} !~ /postponed/);; } #readingsSingleUpdate( $hash, "active", $hash->{status}, 1 ); @@ -4552,6 +4625,7 @@ netatmo_pollGlobal($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/getstationsdata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -4573,6 +4647,7 @@ netatmo_pollGlobalHealth($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/gethomecoachsdata", + timeout => 20, noshutdown => 1, data => { access_token => $hash->{access_token}, }, hash => $hash, @@ -4609,6 +4684,7 @@ netatmo_pollForecast($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/simplifiedfuturemeasure", + timeout => 20, noshutdown => 1, data => { device_id => $hash->{Station}, }, header => "Authorization: Bearer ".$iohash->{access_token_app}, @@ -4629,7 +4705,7 @@ netatmo_pollHome($) if( $hash->{Home} ) { - return undef if($hash->{status} =~ /usage/ || $hash->{status} =~ /delay/); + return undef if($hash->{status} =~ /usage/ || $hash->{status} =~ /postponed/); my $lastupdate = ReadingsVal( $hash->{NAME}, ".lastupdate", undef ); $lastupdate = (time-7*24*60*60) if(!$lastupdate); @@ -4647,7 +4723,7 @@ netatmo_pollRelay($) Log3 $name, 5, "$name: pollrelay ".$hash->{Relay}; if( $hash->{Relay} ) { - return undef if(defined($hash->{status}) && ($hash->{status} =~ /usage/ || $hash->{status} =~ /delay/)); + return undef if(defined($hash->{status}) && ($hash->{status} =~ /usage/ || $hash->{status} =~ /postponed/)); my $lastupdate = ReadingsVal( $hash->{NAME}, ".lastupdate", undef ); $lastupdate = (time-7*24*60*60) if(!$lastupdate); @@ -4665,7 +4741,7 @@ netatmo_pollPerson($) my $name = $hash->{NAME}; Log3 $name, 5, "$name: pollperson "; - return undef if(defined($hash->{status}) && ($hash->{status} =~ /usage/ || $hash->{status} =~ /delay/)); + return undef if(defined($hash->{status}) && ($hash->{status} =~ /usage/ || $hash->{status} =~ /postponed/)); if( $hash->{Home} ) { my $lastupdate = ReadingsVal( $hash->{NAME}, ".lastupdate", undef ); @@ -4708,7 +4784,7 @@ netatmo_Get($$@) } if( $cmd eq "video" || $cmd eq "video_local" ) { - return "no video_id was passed" if($args[0] eq ""); + return "no video_id was passed" if(!defined($args[0]) || $args[0] eq ""); return netatmo_getCameraVideo($hash,$args[0],$cmd); } elsif( $cmd eq "live" || $cmd eq "live_local" ) { @@ -5032,6 +5108,7 @@ netatmo_registerWebhook($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/addwebhook", + timeout => 20, noshutdown => 1, data => { access_token => $iohash->{access_token}, url => $webhookurl, app_type => 'app_security', }, hash => $hash, @@ -5056,6 +5133,7 @@ netatmo_dropWebhook($) HttpUtils_NonblockingGet({ url => "https://api.netatmo.com/api/dropwebhook", + timeout => 20, noshutdown => 1, data => { access_token => $iohash->{access_token}, app_type => 'app_security', }, hash => $hash, @@ -5138,7 +5216,7 @@ sub netatmo_Webhook() { if(AttrVal($name,"webhookPoll","0") eq "1" && defined($json->{home_id})) { my $home = $modules{$hash->{TYPE}}{defptr}{"H$json->{home_id}"}; - netatmo_poll($home) if($home->{status} !~ /usage/ && $home->{status} !~ /delay/);; + netatmo_poll($home) if($home->{status} !~ /usage/ && $home->{status} !~ /postponed/);; } diff --git a/fhem/HISTORY b/fhem/HISTORY index ba2745c1f..9612be6cd 100644 --- a/fhem/HISTORY +++ b/fhem/HISTORY @@ -722,4 +722,7 @@ Resol and compatible VBUS Devices via VBUSIF.pm - Mon Jan 16 2017 (loredo) - - adding new module 98_powerMap + - adding new module 98_powerMap + +- Sun Feb 19 2017 (markus-m) + - replaced module 32_withings with new version