From 9646c3304b2db763f8141e0172c1556310d0d429 Mon Sep 17 00:00:00 2001 From: moises <> Date: Fri, 11 Jan 2019 21:34:24 +0000 Subject: [PATCH] 98_livetracking: added Life360 support; 32_withings: improved Aura handling, added new readings git-svn-id: https://svn.fhem.de/fhem/trunk@18217 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/32_withings.pm | 1312 +++++++++++++++++++++++----------- fhem/FHEM/38_netatmo.pm | 27 +- fhem/FHEM/60_allergy.pm | 2 +- fhem/FHEM/72_XiaomiDevice.pm | 38 +- fhem/FHEM/98_livetracking.pm | 768 +++++++++++++++----- 6 files changed, 1548 insertions(+), 600 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index 122063d7e..75a9afcc6 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # 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. + - change: 32_withings: improve Aura handling - bugfix: 49_SSCam: fix blocking sscam operation if snap was executed with arguments and aatribute snapEmailTxt is not set, (Forum:#45671 #msg885475) diff --git a/fhem/FHEM/32_withings.pm b/fhem/FHEM/32_withings.pm index c29f7e66d..a0f4153b0 100644 --- a/fhem/FHEM/32_withings.pm +++ b/fhem/FHEM/32_withings.pm @@ -3,14 +3,15 @@ # # 32_withings.pm # -# 2018 Markus M. +# 2019 Markus M. # Based on original code by justme1968 # # https://forum.fhem.de/index.php/topic,64944.0.html # # ############################################################################## -# Release 08 / 2018-09-28 +# Release 10 / 2019-01-05 + package main; @@ -30,7 +31,9 @@ use Digest::SHA qw(hmac_sha1_base64); #use LWP::Simple; #use HTTP::Request; #use HTTP::Cookies; -#use URI::Escape qw(uri_escape); +use HTTP::Request::Common; +use LWP; +use URI::Escape qw(uri_escape); use Data::Dumper; @@ -51,17 +54,22 @@ my %device_models = ( 1 => { 1 => "Smart Scale", 2 => "Wireless Scale", 3 => "S 2 => { 21 => "Smart Baby Monitor", 22 => "Home", 23 => "Home v2", }, 4 => { 41 => "iOS Blood Pressure Monitor", 42 => "Wireless Blood Pressure Monitor", 43 => "BPM", 44 => "BPM+", }, 16 => { 51 => "Pulse Ox", 52 => "Activite", 53 => "Activite v2", 54 => "Go", 55 => "Steel HR", }, - 32 => { 60 => "Aura", 61 => "Sleep Sensor", 62 => "Sleep Mat", 63 => "Sleep", }, + 32 => { 60 => "Aura", 61 => "Sleep Sensor", 62 => "Aura v2", 63 => "Sleep", }, 64 => { 70 => "Thermo", }, ); #Firmware files: cdnfw_withings_net #Smart Body Analyzer: /wbs02/wbs02_1521.bin - #Blood Pressure Monitor: /wpm02/wpm02_251.bin + #Cardio Scale: /wbs04/wbs04_1751_2NataR.bin + #Blood Pressure Monitor: /wpm02/wpm02_251.bin wpm02/wpm02_421_w1IpDO.bin #Pulse: /wam01/wam01_1761.bin - #Aura: /wsd01/wsd01_607.bin - #Aura Mat: /wsm01/wsm01_711.bin + #Go: /wam02/wam02_590.bin + #Aura: /wsd01/wsd01_607.bin /wsd01/wsd01_1206_xPN4x8.bin + #Aura Mat: /wsm01/wsm01_711.bin /wsm01/wsm01_1231.bin + #Sleep: /wsm02/wsm02_1531_fMvB9s.bin #Home: /wbp02/wbp02_168.bin #Activite: /hwa01/hwa01_1070.bin + #Thermo: /sct01/sct01_1401_ZVjZyU.bin + my %measure_types = ( 1 => { name => "Weight (kg)", reading => "weight", }, 4 => { name => "Height (meter)", reading => "height", }, @@ -71,69 +79,84 @@ my %measure_types = ( 1 => { name => "Weight (kg)", reading => "weight", }, 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 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", }, + 11 => { name => "Heart Rate (bpm)", reading => "heartPulse", }, #vasistas + 12 => { name => "Temperature (°C)", reading => "temperature", }, #getmeashf + 13 => { name => "Humidity (%)", reading => "humidity", }, #getmeashf + 14 => { name => "unknown 14", reading => "unknown14", }, #device? event home - peak sound level? #getmeashf + 15 => { name => "Noise (dB)", reading => "noise", }, #getmeashf 18 => { name => "Weight Objective Speed", reading => "weightObjectiveSpeed", }, 19 => { name => "Breastfeeding (s)", reading => "breastfeeding", }, #baby 20 => { name => "Bottle (ml)", reading => "bottle", }, #baby 22 => { name => "BMI", reading => "bmi", }, #user? goals - 35 => { name => "CO2 (ppm)", reading => "co2", }, - 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", }, + 35 => { name => "CO2 (ppm)", reading => "co2", }, #getmeashf + 36 => { name => "Steps", reading => "steps", dailyreading => "dailySteps", }, #aggregate #vasistas + 37 => { name => "Elevation (m)", reading => "elevation", dailyreading => "dailyElevation", }, #aggregate #vasistas + 38 => { name => "Calories (kcal)", reading => "calories", dailyreading => "dailyCalories", }, #aggregate #vasistas + 39 => { name => "Intensity", reading => "intensity", }, #intraday only #vasistas + 40 => { name => "Distance (m)", reading => "distance", dailyreading => "dailyDistance", }, #aggregate #measure #vasistas + 41 => { name => "Descent (m)", reading => "descent", dailyreading => "dailyDescent", }, #descent #aggregate #measure ??sleepreading! #vasistas + 42 => { name => "Activity Type", reading => "activityType", }, #intraday only 1:walk 2:run #vasistas + 43 => { name => "Duration (s)", reading => "duration", }, #intraday only #vasistas + 44 => { name => "Sleep State", reading => "sleepstate", }, #intraday #aura mat #vasistas + 45 => { name => "unknown 45", reading => "unknown45", },#vasistas + 46 => { name => "User Event", reading => "userEvent", },#appli type only + 47 => { name => "Meal Calories (kcal)", reading => "caloriesMeal", }, 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 + 50 => { name => "Inactive Duration (s)", reading => "durationInactive", dailyreading => "dailyDurationInactive", }, #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", }, #? + 56 => { name => "Ambient light (lux)", reading => "light", }, # aura device #getmeashf + 57 => { name => "Respiratory rate", reading => "breathing", }, # aura mat #measure #vasistas + 58 => { name => "Air Quality (ppm)", reading => "voc", }, # Home Air Quality #getmeashf + 59 => { name => "unknown 59", reading => "unknown59", }, # activity #vasistas + 60 => { name => "PIM movement", reading => "movementPIM", }, # aura mat #measure vasistas 20-200 peak 800 #vasistas + 61 => { name => "Maximum movement", reading => "movementMaximum", }, # aura mat #measure vasistas 10-60 peak 600 #vasistas + 62 => { name => "unknown 62", reading => "unknown62", }, # aura mat #measure vasistas 20-100 #vasistas + 63 => { name => "unknown 63", reading => "unknown63", }, # aura mat #measure vasistas 0-90 #vasistas + 64 => { name => "unknown 64", reading => "unknown64", }, # aura mat #measure vasistas 30-150 #vasistas + 65 => { name => "unknown 65", reading => "unknown65", }, # aura mat #measure vasistas 500-4000 peak 5000 #vasistas + 66 => { name => "Pressure", reading => "pressure", }, # aura & sleep mat #measure vasistas 4000-7000 #vasistas + 67 => { name => "unknown 67", reading => "unknown67", }, # aura mat #measure vasistas 0-100 peak 500 #vasistas + 68 => { name => "unknown 68", reading => "unknown68", }, # aura mat #measure vasistas 0-800 peak 2000 #vasistas + 69 => { name => "unknown 69", reading => "unknown69", }, # aura mat #measure vasistas 0-5000 peak 10000 #vasistas + 70 => { name => "unknown 70", reading => "unknown70", }, #? #vasistas 71 => { name => "Body Temperature (°C)", reading => "bodyTemperature", }, #thermo - 73 => { name => "Skin Temperature (°C)", reading => "skinTemperature", }, #thermo + 72 => { name => "GPS Speed", reading => "speedGPS", }, #vasistas + 73 => { name => "Skin Temperature (°C)", reading => "skinTemperature", }, #thermo #vasistas 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 79 => { name => "unknown 79", reading => "unknown79", }, # body scale 80 => { name => "unknown 80", reading => "unknown80", }, # body scale 86 => { name => "unknown 86", reading => "unknown86", }, # body scale - 87 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, # measures list sleepreading! + 87 => { name => "Active Calories (kcal)", reading => "caloriesActive", dailyreading => "dailyCaloriesActive", }, # measures list sleepreading! #vasistas 88 => { name => "Bone Mass (kg)", reading => "boneMassWeight", }, - 89 => { name => "unknown 89", reading => "unknown89", }, - 90 => { name => "unknown 90", reading => "unknown90", }, #pulse - 91 => { name => "Pulse Wave Velocity (m/s)", reading => "pulseWave", }, # new weight + 89 => { name => "unknown 89", reading => "unknown89", }, #vasistas + 90 => { name => "unknown 90", reading => "unknown90", }, #pulse #vasistas + 91 => { name => "Pulse Wave Velocity (m/s)", reading => "pulseWave", }, 93 => { name => "Muscle Mass (%)", reading => "muscleRatio", }, # cardio scale 94 => { name => "Bone Mass (%)", reading => "boneRatio", }, # cardio scale 95 => { name => "Hydration (%)", reading => "hydration", }, # body water - 122 => { name => "Pulse Transit Time (ms)", reading => "pulseTransitTime", }, + 96 => { name => "Horizontal Radius", reading => "radiusHorizontal", }, #vasistas + 97 => { name => "Altitude", reading => "altitude", }, #vasistas + 98 => { name => "Latitude", reading => "latitude", },#vasistas + 99 => { name => "Longitude", reading => "longitude", },#vasistas + 100 => { name => "Direction", reading => "direction", },#vasistas + 101 => { name => "Vertical Radius", reading => "radiusVertical", },#vasistas + 120 => { name => "unknown 120", reading => "unknown120", }, #vasistas + 121 => { name => "Snoring", reading => "snoring", }, # sleep #vasistas + 122 => { name => "Lean Mass (%)", reading => "fatFreeRatio", }, + 123 => { name => "unknown 123", reading => "unknown123", }, + 124 => { name => "unknown 124", reading => "unknown124", }, + 125 => { name => "unknown 125", reading => "unknown125", }, #-10 => { name => "Speed", reading => "speed", }, #-11 => { name => "Pace", reading => "pace", }, #-12 => { name => "Altitude", reading => "altitude", }, ); + #swimStrokes / swimLaps / walkState / runState my %activity_types = ( 0 => "None", 1 => "Walking", @@ -159,7 +182,7 @@ my %activity_types = ( 0 => "None", 21 => "Soccer", 22 => "Football", 23 => "Rugby", - 24 => "Vollyball", + 24 => "Volleyball", 25 => "Water Polo", 26 => "Horse Riding", 27 => "Golf", @@ -187,7 +210,8 @@ my %activity_types = ( 0 => "None", 271 => "Multi Sports", 272 => "Multi Sport", ); -my %sleep_state = ( 0 => "awake", +my %sleep_state = ( -1 => "unknown", + 0 => "awake", 1 => "light sleep", 2 => "deep sleep", 3 => "REM sleep", ); @@ -248,6 +272,11 @@ my %sleep_readings = ( 'lightsleepduration' => { name => "Light Sleep", reading 'hr_min' => { name => "Minimum HR", reading => "heartrateMinimum", unit => "bpm", }, 'hr_average' => { name => "Average HR", reading => "heartrateAverage", unit => "bpm", }, 'hr_max' => { name => "Maximum HR", reading => "heartrateMaximum", unit => "bpm", }, + 'rr_min' => { name => "Minimum RR", reading => "breathingMinimum", unit => 0, }, + 'rr_average' => { name => "Average RR", reading => "breathingAverage", unit => 0, }, + 'rr_max' => { name => "Maximum RR", reading => "breathingMaximum", unit => 0, }, + 'snoring' => { name => "Snoring", reading => "snoringDuration", unit => "s", }, + 'snoringepisodecount' => { name => "Snoring Episode Count", reading => "snoringEpisodeCount", unit => 0, }, ); my %alarm_sound = ( 0 => "Unknown", @@ -270,7 +299,17 @@ 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)", ); + 4 => "Sacred Forest (20 min)", + 5 => "Spotify", + 6 => "Internet radio", ); + +my %nap_song = ( "Unknown" => 0, + "Celestial Piano (20 min)" => 1, + "Cotton Cloud (10 min)" => 2, + "Deep Smile (10 min)" => 3, + "Sacred Forest (20 min)" => 4, + "Spotify" => 5, + "Internet radio" => 6, ); my %sleep_sound = ( 0 => "Unknown", @@ -280,6 +319,15 @@ my %sleep_sound = ( 0 => "Unknown", 4 => "Cloud Flakes", 5 => "Spotify", 6 => "Internet radio", ); +# +my %sleep_song = ( "Unknown" => 0, + "Moonlight Waves" => 1, + "Siren's Whisper" => 2, + "Celestial Piano" => 3, + "Cloud Flakes" => 4, + "Spotify" => 5, + "Internet radio" => 6, ); + sub withings_Initialize($) { @@ -290,6 +338,7 @@ sub withings_Initialize($) { $hash->{GetFn} = "withings_Get"; $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "withings_Notify"; + $hash->{ReadFn} = "withings_Read"; $hash->{UndefFn} = "withings_Undefine"; $hash->{DbLog_splitFn} = "withings_DbLog_splitFn"; $hash->{AttrFn} = "withings_Attr"; @@ -300,7 +349,10 @@ sub withings_Initialize($) { "intervalDebug ". "intervalProperties ". "intervalDaily ". - "nossl:1 ". + "callback_url ". + "client_id ". + "client_secret ". +# "nossl:1 ". "IP ". "videoLinkEvents:1 "; @@ -353,7 +405,7 @@ sub withings_Define($$) { $hash->{DEF} = "$user $accesskey"; $hash->{User} = $user; - #$hash->{Key} = $accesskey; #not needed + $hash->{helper}{Key} = $accesskey; my $d = $modules{$hash->{TYPE}}{defptr}{"U$user"}; return "device $user already defined as $d->{NAME}" if( defined($d) && $d->{NAME} ne $name ); @@ -391,13 +443,13 @@ sub withings_Define($$) { #CommandAttr(undef,"$name DbLogExclude .*"); - my $resolve = inet_aton("scalews.withings.com"); - if(!defined($resolve)) - { - $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); - InternalTimer( gettimeofday() + 900, "withings_InitWait", $hash, 0); - return undef; - } + # my $resolve = inet_aton("scalews.withings.com"); + # if(!defined($resolve)) + # { + # $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); + # InternalTimer( gettimeofday() + 900, "withings_InitWait", $hash, 0); + # return undef; + # } $hash->{STATE} = "Initialized" if( $hash->{SUBTYPE} eq "ACCOUNT" ); @@ -406,12 +458,23 @@ sub withings_Define($$) { 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" ); + + #connect aura + my $auraip = $attr{$name}{IP}; + if($auraip){ + $hash->{DeviceName} = $auraip.":7685"; + + Log3 $hash, 3, "$name: Opening Aura socket"; + withings_Close($hash) if(DevIo_IsOpen($hash)); + withings_Open($hash); + } } else { InternalTimer(gettimeofday()+15, "withings_InitWait", $hash, 0); } + withings_addExtension($hash) if( $hash->{SUBTYPE} eq "ACCOUNT" ); return undef; } @@ -419,23 +482,35 @@ sub withings_Define($$) { sub withings_InitWait($) { my ($hash) = @_; - Log3 "withings", 5, "withings: initwait ".$init_done; + my $name= $hash->{NAME}; - RemoveInternalTimer($hash); + Log3 $name, 5, "$name: initwait ".$init_done; - my $resolve = inet_aton("scalews.withings.com"); - if(!defined($resolve)) - { - $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); - InternalTimer( gettimeofday() + 1800, "withings_InitWait", $hash, 0); - return undef; - } + RemoveInternalTimer($hash,"withings_InitWait"); + + # my $resolve = inet_aton("scalews.withings.com"); + # if(!defined($resolve)) + # { + # $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); + # 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" ); + + #connect aura + my $auraip = $attr{$name}{IP}; + if($auraip){ + $hash->{DeviceName} = $auraip.":7685"; + + Log3 $hash, 3, "$name: Opening Aura socket"; + withings_Close($hash) if(DevIo_IsOpen($hash)); + withings_Open($hash); + } } else { @@ -448,23 +523,109 @@ sub withings_InitWait($) { sub withings_Notify($$) { my ($hash,$dev) = @_; + my $name= $hash->{NAME}; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); - Log3 "withings", 5, "withings: notify"; + Log3 $name, 5, "$name: notify"; - my $resolve = inet_aton("scalews.withings.com"); - if(!defined($resolve)) - { - $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); - InternalTimer( gettimeofday() + 3600, "withings_InitWait", $hash, 0); - return undef; - } + # my $resolve = inet_aton("scalews.withings.com"); + # if(!defined($resolve)) + # { + # $hash->{STATE} = "DNS error" if( $hash->{SUBTYPE} eq "ACCOUNT" ); + # 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" ); + + #connect aura + my $auraip = $attr{$name}{IP}; + if($auraip){ + $hash->{DeviceName} = $auraip.":7685"; + + Log3 $hash, 3, "$name: Opening Aura socket"; + withings_Close($hash) if(DevIo_IsOpen($hash)); + withings_Open($hash); + } + +} + +sub withings_Open($) { + my ($hash) = @_; + my $name= $hash->{NAME}; + + return undef if(DevIo_IsOpen($hash)); + my $auraip = AttrVal($name,"IP",undef); + if($auraip){ + $hash->{DeviceName} = $auraip.":7685"; + + Log3 $hash, 2, "$name: Reopening Aura socket"; + DevIo_OpenDev($hash, 0, "withings_Hello", "withings_Callback"); + } + return undef; +} + +sub withings_Hello($) { + my ($hash) = @_; + my $name= $hash->{NAME}; + + my $data = "000100010100050101010000"; #hello + withings_Write($hash, $data); + + $data="010100050101110000"; #hello2 + withings_Write($hash, $data); + + $data="0101000a01090a0005090a000100"; #ping + withings_Write($hash, $data); + + return undef; +} + +sub withings_Close($) { + my ($hash) = @_; + my $name= $hash->{NAME}; + + DevIo_CloseDev($hash) if(DevIo_IsOpen($hash)); + return undef; +} + + +sub withings_Read($) { + my ($hash) = @_; + my $name= $hash->{NAME}; + my $buf; + $buf = DevIo_SimpleRead($hash); + return undef if(!defined($buf)); + + Log3 $hash, 4, "$name: Received " . length($buf) . " bytes: ".unpack('H*', $buf) if(length($buf) > 1); + + if(length($buf) > 1) { + withings_parseAuraData($hash,$buf); + } + + return undef; +} + +sub withings_Write($$) { + my ($hash,$data) = @_; + my $name= $hash->{NAME}; + Log3 $hash, 4, "$name: Written " . length($data) . " bytes: ".$data if(length($data) > 1); + $data = pack('H*', $data); + DevIo_SimpleWrite($hash,$data,0); + return undef; +} + + + +sub withings_Callback($) { + my ($hash, $error) = @_; + my $name = $hash->{NAME}; + Log3 $name, 2, "$name: error while connecting to Aura: $error" if($error); + return undef; } @@ -475,6 +636,7 @@ sub withings_Undefine($$) { 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" ); + DevIo_CloseDev($hash); return undef; } @@ -485,12 +647,12 @@ sub withings_getToken($) { Log3 "withings", 5, "withings: gettoken"; - my $resolve = inet_aton("auth.withings.com"); - if(!defined($resolve)) - { - Log3 "withings", 1, "withings: DNS error on getToken"; - return undef; - } + # 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", @@ -528,13 +690,13 @@ sub withings_getSessionKey($) { return if( $hash->{SessionKey} && $hash->{SessionTimestamp} && gettimeofday() - $hash->{SessionTimestamp} < (60*60*24*7-3600) ); - my $resolve = inet_aton("account.withings.com"); - if(!defined($resolve)) - { - $hash->{SessionTimestamp} = 0; - Log3 $name, 1, "$name: DNS error on getSessionData"; - return undef; - } + # my $resolve = inet_aton("account.withings.com"); + # if(!defined($resolve)) + # { + # $hash->{SessionTimestamp} = 0; + # Log3 $name, 1, "$name: DNS error on getSessionData"; + # return undef; + # } $hash->{'.https'} = "https" if(!defined($hash->{'.https'})); @@ -887,7 +1049,15 @@ sub withings_initDevice($) { if(defined($devicelink) && defined($devicelink->{linkuserid})) { $hash->{User} = $devicelink->{linkuserid}; - $hash->{UserDevice} = $modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{linkuserid}} if defined($modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{linkuserid}}); + my $userhash = $modules{$hash->{TYPE}}{defptr}{"U".$devicelink->{linkuserid}}; + if(defined($userhash)){ + $hash->{UserDevice} = $userhash; + if(defined($hash->{typeID}) && $hash->{typeID} == 16){ + $userhash->{Tracker} = $hash->{Device}; + } elsif(defined($hash->{typeID}) && $hash->{typeID} == 32 && defined($hash->{modelID}) && $hash->{modelID} != 60) { + $userhash->{Sleep} = $hash->{Device}; + } + } } } @@ -935,6 +1105,8 @@ sub withings_initUser($) { $attr{$name}{stateFormat} = "weight kg" if( !defined( $attr{$name}{stateFormat} ) ); InternalTimer(gettimeofday()+60, "withings_poll", $hash, 0); + withings_AuthRefresh($hash) if(defined(ReadingsVal($name,".refresh_token",undef))); + } @@ -1143,10 +1315,10 @@ sub withings_getDeviceProperties($) { } -sub withings_getDeviceReadingsScale($) { +sub withings_getDeviceReadingsGeneric($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getscalereadings ".$hash->{Device}; + Log3 $name, 5, "$name: getdevicereadings ".$hash->{Device}; return undef if( !defined($hash->{Device}) ); return undef if( !defined($hash->{IODev}) ); @@ -1162,9 +1334,9 @@ sub withings_getDeviceReadingsScale($) { url => "https://scalews.withings.com/cgi-bin/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}->{helper}{appliver}, apppfm => 'web', action => 'getmeashf'}, + data => {sessionid => $hash->{IODev}->{SessionKey}, deviceid=> $hash->{Device}, meastype => '12,13,14,15,35,56,58,74,75', startdate => int($lastupdate), enddate => int($enddate), devicetype => '16', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getmeashf'}, hash => $hash, - type => 'deviceReadingsScale', + type => 'deviceReadingsGeneric', enddate => int($enddate), callback => \&withings_Dispatch, }); @@ -1178,75 +1350,6 @@ sub withings_getDeviceReadingsScale($) { } -sub withings_getDeviceReadingsBedside($) { - my ($hash) = @_; - my $name = $hash->{NAME}; - Log3 $name, 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://scalews.withings.com/cgi-bin/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}->{helper}{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 $name, 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://scalews.withings.com/cgi-bin/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}->{helper}{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) = @_; @@ -1482,7 +1585,7 @@ sub withings_poll($;$) { $force = 0 if(!defined($force)); my $name = $hash->{NAME}; - RemoveInternalTimer($hash); + RemoveInternalTimer($hash, "withings_poll"); return undef if(IsDisabled($name)); @@ -1509,7 +1612,7 @@ sub withings_poll($;$) { if(defined($hash->{modelID}) && $hash->{modelID} eq '4') { withings_getDeviceProperties($hash) if($force > 1 || $lastProperties <= ($now - $intervalProperties)); - withings_getDeviceReadingsScale($hash) if($force || $lastData <= ($now - $intervalData)); + withings_getDeviceReadingsGeneric($hash) if($force || $lastData <= ($now - $intervalData)); } elsif(defined($hash->{modelID}) && $hash->{modelID} eq '21') { my $intervalAlert = AttrVal($name,"intervalAlert",120); @@ -1522,7 +1625,7 @@ sub withings_poll($;$) { 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_getDeviceReadingsGeneric($hash) if($force || $lastData <= ($now - $intervalData)); withings_getDeviceAlertsHome($hash) if($force || $lastAlert <= ($now - $intervalAlert)); } elsif(defined($hash->{typeID}) && $hash->{typeID} eq '16') { @@ -1531,16 +1634,20 @@ sub withings_poll($;$) { } 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)); + withings_getDeviceReadingsGeneric($hash) if($force || $lastData <= ($now - $intervalData)); } elsif(defined($hash->{modelID}) && ($hash->{modelID} eq '61' || $hash->{modelID} eq '62' || $hash->{modelID} eq '63')) { 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)); + #if(defined($hash->{modelID}) && ($hash->{modelID} eq '63')){ + # withings_getDeviceReadingsGeneric($hash) if($force || $lastData <= ($now - $intervalData)); + #} } else { withings_getDeviceProperties($hash) if($force || $lastProperties <= ($now - $intervalProperties)); + withings_getDeviceReadingsGeneric($hash) if($force || $lastData <= ($now - $intervalData)); } } elsif( $hash->{SUBTYPE} eq "DUMMY" ) { my $intervalData = AttrVal($name,"intervalData",900); @@ -1569,8 +1676,9 @@ sub withings_poll($;$) { sub withings_getUserReadingsDaily($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getuserdailystats ".$hash->{User}; + Log3 $name, 5, "$name: getuserdailystats ".$hash->{User} if(defined($hash->{User})); + return undef if( !defined($hash->{User}) ); return undef if( !defined($hash->{IODev}) ); withings_getSessionKey( $hash->{IODev} ); @@ -1611,17 +1719,6 @@ sub withings_getUserReadingsDaily($) { callback => \&withings_Dispatch, }); -# HttpUtils_NonblockingGet({ -# url => "https://scalews.withings.com/cgi-bin/v2/activity", -# timeout => 60, -# noshutdown => 1, -# data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, startdateymd => $startdateymd, enddateymd => $enddateymd, appname => 'hmw', appliver => $hash->{IODev}->{helper}{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 ); @@ -1636,8 +1733,9 @@ sub withings_getUserReadingsDaily($) { sub withings_getUserReadingsCommon($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getuserreadings ".$hash->{User}; + Log3 $name, 5, "$name: getuserreadings ".$hash->{User} if(defined($hash->{User})); + return undef if( !defined($hash->{User}) ); return undef if( !defined($hash->{IODev}) ); withings_getSessionKey( $hash->{IODev} ); @@ -1672,15 +1770,16 @@ sub withings_getUserReadingsCommon($) { sub withings_getUserReadingsSleep($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getsleepreadings ".$hash->{User}; + Log3 $name, 5, "$name: getsleepreadings ".$hash->{User} if(defined($hash->{User})); + return undef if( !defined($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)); + my $enddate = ($lastupdate+(24*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}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'}, @@ -1688,7 +1787,7 @@ sub withings_getUserReadingsSleep($) { url => "https://scalews.withings.com/cgi-bin/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}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'}, + data => {sessionid => $hash->{IODev}->{SessionKey}, userid=> $hash->{User}, meastype => '11,39,41,43,44,57,59,87,121', startdate => int($lastupdate), enddate => int($enddate), devicetype => '32', appname => 'my2', appliver => $hash->{IODev}->{helper}{appliver}, apppfm => 'web', action => 'getvasistas'}, hash => $hash, type => 'userReadingsSleep', enddate => int($enddate), @@ -1707,15 +1806,16 @@ sub withings_getUserReadingsSleep($) { sub withings_getUserReadingsSleepDebug($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getsleepreadingsdebug ".$hash->{User}; + Log3 $name, 5, "$name: getsleepreadingsdebug ".$hash->{User} if(defined($hash->{User})); + return undef if( !defined($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)); + my $enddate = ($lastupdate+(24*60*60)); $enddate = $now if ($enddate > $now); HttpUtils_NonblockingGet({ @@ -1743,15 +1843,16 @@ sub withings_getUserReadingsActivity($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: getactivityreadings ".$hash->{User}; + Log3 $name, 5, "$name: getactivityreadings ".$hash->{User} if(defined($hash->{User})); + return undef if( !defined($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)); + my $enddate = ($lastupdate+(24*60*60)); $enddate = $now if ($enddate > $now); Log3 $name, 5, "$name: getactivityreadings ".$lastupdate." to ".$enddate; @@ -1826,7 +1927,7 @@ sub withings_parseMeasureGroups($$) { foreach my $measure (@{$measuregrp->{measures}}) { my $reading = $measure_types{$measure->{type}}->{reading}; if( !defined($reading) ) { - Log3 $name, 1, "$name: unknown measure type: $measure->{type}"; + Log3 $name, 1, "$name: unknown measure type: $measure->{type} ".Dumper($measure); next; } @@ -1891,7 +1992,7 @@ sub withings_parseMeasurements($$) { foreach my $series ( @{$json->{body}{series}}) { my $reading = $measure_types{$series->{type}}->{reading}; if( !defined($reading) ) { - Log3 $name, 1, "$name: unknown measure type: $series->{type}"; + Log3 $name, 1, "$name: unknown measure type: $series->{type} ".Dumper($series); next; } @@ -2010,7 +2111,7 @@ sub withings_parseAggregate($$) { #my $timestamp = $dayhash->{midnight}; my $reading = $measure_types{$typestring}->{dailyreading}; if( !defined($reading) ) { - Log3 $name, 1, "$name: unknown measure type: $typestring"; + Log3 $name, 1, "$name: unknown aggregate measure type: $typestring"; next; } my $value = $dayhash->{sum}; @@ -2101,11 +2202,12 @@ sub withings_parseActivity($$) { next; } + my $duration = 0; foreach my $dataset ( keys (%{$series->{data}})) { if(!defined($sleep_readings{$dataset}->{reading})) { - Log3 $name, 2, "$name: unknown sleep reading $dataset"; + Log3 $name, 2, "$name: unknown activity reading $dataset"; next; } @@ -2115,8 +2217,22 @@ sub withings_parseActivity($$) { my $value = $series->{data}{$dataset}; push(@readings, [$timestamp, $reading, $value]); + + if($reading eq "sleepDurationLight" || $reading eq "sleepDurationDeep" || $reading eq "sleepDurationREM"){ + $duration += $value; + } + } + my ($year,$mon,$day) = split(/[\s-]+/, $series->{date}); + my $timestamp = timelocal(0,0,6,$day,$mon-1,$year-1900); + push(@readings, [$timestamp, "sleepDurationTotal", $duration]); + + if(defined($series->{sleep_score})){ + if(defined($series->{sleep_score}{score})){ + push(@readings, [$timestamp, "sleepScore", $series->{sleep_score}{score}]); + } + } } @@ -2176,7 +2292,6 @@ sub withings_parseWorkouts($$) { Log3 $name, 1, "$name: parseworkouts\n".Dumper($json); return undef; - } @@ -2228,6 +2343,10 @@ sub withings_parseVasistas($$;$) { Log3 $name, 1, "$name: unknown measure type: $readingstype"; next; } + if($updatetype eq "pressure") { + $updatevalue = $updatevalue * 0.01; + Log3 $name, 5, "$name: Aura reading calculated ".$updatetime.' '.$updatetype.': '.$updatevalue; + } if(($updatetype eq "breathing") and ($updatevalue > 90)) { Log3 $name, 2, "$name: Implausible Aura reading ".$updatetime.' '.$updatetype.': '.$updatevalue; $newlastupdate = $readingsdate if($readingsdate > $newlastupdate); @@ -2521,8 +2640,8 @@ sub withings_Get($$@) { my $list; if( $hash->{SUBTYPE} eq "USER" ) { - $list = "update:noArg updateAll:noArg"; - + $list = "update:noArg updateAll:noArg showKey:noArg"; + $list .= " showSubscriptions:noArg" if($hash->{helper}{OAuthKey}); if( $cmd eq "updateAll" ) { withings_poll($hash,2); return undef; @@ -2531,6 +2650,15 @@ sub withings_Get($$@) { withings_poll($hash,1); return undef; } + if( $cmd eq 'showKey' ) + { + my $key = $hash->{helper}{Key}; + return 'no key set' if( !$key ); + $key = withings_decrypt( $key ); + return "key: $key"; + } + return withings_AuthList($hash) if($cmd eq "showSubscriptions"); + } 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'); @@ -2621,7 +2749,8 @@ sub withings_Set($$@) { 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 = " on:noArg off:noArg reset:noArg rgb:colorpicker,RGB"; + $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"; @@ -2629,6 +2758,7 @@ sub withings_Set($$@) { $list .= " flashMat"; $list .= " sensors:on,off"; $list .= " rawCmd"; + $list .= " reconnect:noArg"; if (defined($hash->{helper}{ALARMSCOUNT})&&($hash->{helper}{ALARMSCOUNT}>0)) { for(my $i=1;$i<=$hash->{helper}{ALARMSCOUNT};$i++) @@ -2640,14 +2770,27 @@ sub withings_Set($$@) { } - 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' ) + if ( $cmd eq 'reconnect' ) + { + withings_Close($hash) if(DevIo_IsOpen($hash)); + withings_Open($hash); + return undef; + } + elsif ( $cmd eq 'rgb' ) + { + return withings_setAuraAlarm($hash,$cmd,join( "", @arg )); + } + if ( lc $cmd eq 'on' or lc $cmd eq 'off' or lc $cmd eq 'reset' ) + { + return withings_setAuraAlarm($hash,$cmd); + } + elsif ( 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 ) #{ @@ -2670,8 +2813,18 @@ sub withings_Set($$@) { return "Unknown argument $cmd, choose one of $list"; } elsif($hash->{SUBTYPE} eq "ACCOUNT") { $list = "autocreate:noArg"; + $list .= " authorize:noArg" if(AttrVal($name,"client_id",undef)); + return withings_AuthApp($hash,join( "", @arg )) if($cmd eq "authorize"); return withings_autocreate($hash) if($cmd eq "autocreate"); return "Unknown argument $cmd, choose one of $list"; + } elsif($hash->{SUBTYPE} eq "USER" && defined(ReadingsVal($name,".refresh_token",undef))) { + $list = "login:noArg"; + $list .= " subscribe:noArg unsubscribe:noArg" if(defined($hash->{helper}{OAuthKey})); + + return withings_AuthRefresh($hash) if($cmd eq "login"); + return withings_AuthUnsubscribe($hash) if($cmd eq "unsubscribe"); + return withings_AuthSubscribe($hash) if($cmd eq "subscribe"); + return "Unknown argument $cmd, choose one of $list"; } else { return "Unknown argument $cmd, choose one of $list"; } @@ -2682,48 +2835,149 @@ sub withings_readAuraAlarm($) { my ($hash) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: readauraalarm"; + Log3 $name, 4, "$name: readAuraAlarm"; - my $auraip = AttrVal($name,"IP",undef); - return if(!$auraip); + withings_Open($hash) if(!DevIo_IsOpen($hash)); - 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(); + withings_Write($hash, $data); $data="010100050101110000"; #hello2 - $socket->send(pack('H*', $data)); - $socket->flush(); - $socket->recv($data, 1024); - $socket->flush(); - + withings_Write($hash, $data); $data="0101000a01090a0005090a000100"; #ping - $socket->send(pack('H*', $data)); - $socket->flush(); - $socket->recv($data, 1024); - $socket->flush(); + withings_Write($hash, $data); + $data="010100050101250000"; #new alarmdata - $socket->send(pack('H*', $data)); - $socket->flush(); + withings_Write($hash, $data); + $data="010100050109070000"; #getstate + withings_Write($hash, $data); + $data="010100050109100000"; #sensordata clock + withings_Write($hash, $data); + $data="0101000b0109060006090800020300"; #sleepdata + withings_Write($hash, $data); + $data="0101000b0109060006090800020200"; #napdata + withings_Write($hash, $data); - $socket->recv($data, 1024); - $socket->flush(); + $data="0101000a01090a0005090a000100"; #ping + withings_Write($hash, $data); + #$data="0101000b0109060006090800020000"; #unknown + #withings_Write($hash, $data); + #0101000f010100000a011000060906fffffffe + #$data="0101000b0109060006090800020100"; #unknown + #withings_Write($hash, $data); + #0101000d01090600080906000432320101 + #$data="0101000b0109060006090800020400"; #unknown + #withings_Write($hash, $data); + #0101000f010100000a011000060906fffffffe + #$data="010100050109010000"; #getstate + #withings_Write($hash, $data); + return undef; +} + +sub withings_parseAuraData($$) { + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + $data = unpack('H*', $data); + + Log3 $name, 5, "$name: parseAuraData $data"; + + if( + $data eq "010100050109030000" || #alarm/nap/sleep answer + $data eq "010100050109040000" || #stop answer + $data eq "010100050109050000" || #settings1 answer + $data eq "0101000501090a0000" || #settings2 answer + $data eq "0101000501090d0000" || #light answer + $data eq "0101000501090f0000" || #clock/sensors answer + $data eq "010100050109110000" || #snooze answer + $data eq "0101000b0101110006011800020000" || #init + $data eq "0101000f010100000a011000060125fffffffe" || #hello + $data eq "0101000f010100000a01100006090afffffffe" || #hello + $data eq "") { + #set/ping/init return + return undef; + } + elsif($data =~ /x0101004a01010100450101/){ + #init info + return undef; + } + elsif($data =~ /01010010010910000b090d/) { #sensor & clock data + Log3 $name, 4, "$name: sensor data ".$data; + $data = pack('H*', $data); + my $clockdisplay = ord(substr($data,13,1)); + my $clockbrightness = ord(substr($data,14,1)); + my $sensors = (ord(substr($data,19,1))==0)?"on":"off"; + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "clock_state", ($clockdisplay ? "on":"off"), 1 ); + readingsBulkUpdate( $hash, "clock_brightness", $clockbrightness, 1 ); + readingsBulkUpdate( $hash, "sensors", $sensors, 1 ); + readingsEndUpdate($hash,1); + } + elsif($data =~ /0101001701090700120907/) { #alarm state + Log3 $name, 4, "$name: alarm state ".$data; + $data = pack('H*', $data); + my $devicestate = ord(substr($data,18,1)); + my $alarmtype = ord(substr($data,13,1)); + my $lightstate = ord(substr($data,26,1)); + + if($devicestate eq 0) + { + readingsSingleUpdate( $hash, "state", "off", 1 ); + } + elsif($devicestate eq 2) + { + readingsSingleUpdate( $hash, "state", "snoozed", 1 ); + } + elsif($devicestate eq 1) + { + readingsSingleUpdate( $hash, "state", "sleep", 1 ) if($alarmtype eq 1); + readingsSingleUpdate( $hash, "state", "alarm", 1 ) if($alarmtype eq 2); + readingsSingleUpdate( $hash, "state", "nap", 1 ) if($alarmtype eq 3); + } + + #if($lightstate eq 1){ + # readingsSingleUpdate( $hash, "power", "off", 1 ); + #} else { + # readingsSingleUpdate( $hash, "power", "on", 1 ); + #} + } + elsif($data =~ /0101000d01090600080906/) { #sleep/nap data + $data = pack('H*', $data); + if(ord(substr($data,15,1)) == 3){ + Log3 $name, 4, "$name: sleep data ".unpack('H*', $data); + my $sleepvolume = ord(substr($data,13,1)); + my $sleepbrightness = ord(substr($data,14,1)); + my $sleepsong = ord(substr($data,16,1)); + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "sleep_volume", $sleepvolume, 1 ); + readingsBulkUpdate( $hash, "sleep_brightness", $sleepbrightness, 1 ); + readingsBulkUpdate( $hash, "sleep_sound", $sleep_sound{$sleepsong}, 1 ); + readingsEndUpdate($hash,1); + } + elsif(ord(substr($data,15,1)) == 2){ + Log3 $name, 4, "$name: nap data ".unpack('H*', $data); + my $napvolume = ord(substr($data,13,1)); + my $napbrightness = ord(substr($data,14,1)); + my $napsong = ord(substr($data,16,1)); + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, "nap_volume", $napvolume, 1 ); + readingsBulkUpdate( $hash, "nap_brightness", $napbrightness, 1 ); + readingsBulkUpdate( $hash, "nap_sound", $nap_sound{$napsong}, 1 ); + readingsEndUpdate($hash,1); + } + else { + Log3 $name, 1, "$name: unknown sleep/nap data ".unpack('H*', $data); + } + } + elsif($data =~ /010100..01012500/) { + Log3 $name, 4, "$name: alarm data ".$data; + $data = pack('H*', $data); my $datalength = ord(substr($data,2,1))*256 + ord(substr($data,3,1)); Log3 $name, 5, "$name: alarmdata ($datalength)".unpack('H*', $data); @@ -2789,100 +3043,21 @@ sub withings_readAuraAlarm($) { $alarmcounter++; } - + readingsEndUpdate($hash,1); 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); + else { + Log3 $name, 2, "$name: unknown aura data $data"; } - readingsEndUpdate($hash,1); - $socket->close(); - return; + return undef; } @@ -2890,41 +3065,39 @@ sub withings_setAuraAlarm($$;$) { my ($hash, $setting, $value) = @_; my $name = $hash->{NAME}; - Log3 $name, 5, "$name: setaura ".$setting; + Log3 $name, 5, "$name: setAuraAlarm ".$setting; - my $auraip = AttrVal($name,"IP",undef); - return if(!$auraip); + withings_Open($hash) if(!DevIo_IsOpen($hash)); - 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="010100050109070000"; #getstate - 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") + if($setting eq "on") + { + $data="0101000a01090d0005090c000101"; #on + } + elsif($setting eq "off") + { + $data="0101000a01090d0005090c000100"; #off + } + elsif($setting eq "reset") + { + $data="0101000f01090d000a09140006000000000000"; #reset + readingsSingleUpdate( $hash, "rgb", "000000", 1 ); + } + elsif($setting eq "rgb") + { + if(defined($value) && length($value) == 6 && $value =~ /^[0-9a-fA-F]+/) { + my $r = lc(substr( $value,0,2 )); + my $g = lc(substr( $value,2,2 )); + my $b = lc(substr( $value,4,2 )); + $data="0101001401090d000f09140006".$r."00".$g."00".$b;#."00090C000101"; + readingsSingleUpdate( $hash, "rgb", lc($value), 1 ); + } else { + $data="0101000f01090d000a09140006000000000000"; + readingsSingleUpdate( $hash, "rgb", "000000", 1 ); + } + } + elsif($setting eq "nap") { $data="0101000b0109030006090800020200"; #nap } @@ -3030,7 +3203,7 @@ sub withings_setAuraAlarm($$;$) { $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)); + $data .= sprintf("%.2x",$nap_song{ReadingsVal( $name, "nap_sound", "Unknown")eq"Unknown"?"Celestial Piano (20 min)":ReadingsVal( $name, "nap_sound", "Celestial Piano (20 min)")}); } elsif($setting =~ /^sleep/) { @@ -3038,7 +3211,7 @@ sub withings_setAuraAlarm($$;$) { $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)); + $data .= sprintf("%.2x",$sleep_song{ReadingsVal( $name, "sleep_sound", "Unknown")eq"Unknown"?"Moonlight Waves":ReadingsVal( $name, "sleep_sound", "Moonlight Waves")}); } elsif($setting =~ /^clock/) { @@ -3060,20 +3233,16 @@ sub withings_setAuraAlarm($$;$) { $data = "0101000a01090f0005080b000101" if($value eq "off"); } - Log3 $name, 3, "$name: writesocket ".$data; + Log3 $name, 5, "$name: Write Aura socket: ".$data; + withings_Write($hash, $data); + $data="0101000a01090a0005090a000100"; #ping + withings_Write($hash, $data); - $socket->send(pack('H*', $data)); - $socket->flush(); + #withings_Close($hash) if(DevIo_IsOpen($hash)); - $socket->recv($data, 1024); - $socket->flush(); - - Log3 $name, 4, "$name: readsocket ".unpack('H*', $data); - - $socket->close(); - return; + return undef; } @@ -3081,51 +3250,20 @@ sub withings_setAuraDebug($$;$) { my ($hash, $value) = @_; my $name = $hash->{NAME}; - my $auraip = AttrVal($name,"IP",undef); - return if(!$auraip); + withings_Open($hash) if(!DevIo_IsOpen($hash)); - 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=$value; #debug + Log3 $name, 2, "$name: Write Aura socket debug ".$data; + Log3 $name, 5, "$name: Write Aura socket debug ".pack('H*', $data); - my $data = "000100010100050101010000"; #hello - $socket->send(pack('H*', $data)); - $socket->flush(); - $socket->recv($data,1024); - $socket->flush(); + withings_Write($hash, $data); - $data="010100050101110000"; #hello2 - $socket->send(pack('H*', $data)); - $socket->flush(); - $socket->recv($data,1024); - $socket->flush(); + #$data="0101000a01090a0005090a000100"; #ping + #withings_Write($hash, $data); - $data="0101000a01090a0005090a000100"; #ping - $socket->send(pack('H*', $data)); - $socket->flush(); - $socket->recv($data,1024); - $socket->flush(); + #withings_Close($hash) if(DevIo_IsOpen($hash)); - $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; + return undef; } @@ -3148,6 +3286,7 @@ sub withings_Attr($$$) { } else { $attr{$name}{$attrName} = 0; withings_poll($hash,0); + withings_AuthRefresh($hash) if(defined(ReadingsVal($name,".refresh_token",undef))); } } elsif( $attrName eq "nossl" ) { @@ -3208,7 +3347,7 @@ sub withings_Dispatch($$$) { $json->{requestedenddate} = $param->{enddate}; } - if( $param->{type} eq 'deviceReadingsScale' || $param->{type} eq 'deviceReadingsBedside' || $param->{type} eq 'deviceReadingsHome' ) { + if( $param->{type} eq 'deviceReadingsGeneric' ) { withings_parseMeasurements($hash, $json); } elsif( $param->{type} eq 'userReadingsSleep' || $param->{type} eq 'userReadingsSleepDebug' || $param->{type} eq 'userReadingsActivity' ) { withings_parseVasistas($hash, $json, $param->{type}); @@ -3232,8 +3371,7 @@ sub withings_Dispatch($$$) { -sub withings_encrypt($) -{ +sub withings_encrypt($) { my ($decoded) = @_; my $key = getUniqueId(); my $encoded; @@ -3249,8 +3387,7 @@ sub withings_encrypt($) return 'crypt:'.$encoded; } -sub withings_decrypt($) -{ +sub withings_decrypt($) { my ($encoded) = @_; my $key = getUniqueId(); my $decoded; @@ -3269,6 +3406,361 @@ sub withings_decrypt($) } +######################### +sub withings_addExtension($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + #withings_removeExtension() ; + my $url = "/withings"; + delete $data{FWEXT}{$url} if($data{FWEXT}{$url}); + + Log3 $name, 2, "Enabling Withings webcall for $name"; + $data{FWEXT}{$url}{deviceName} = $name; + $data{FWEXT}{$url}{FUNC} = "withings_Webcall"; + $data{FWEXT}{$url}{LINK} = "withings"; + + $modules{"withings"}{defptr}{"webcall"} = $hash; + +} + +######################### +sub withings_removeExtension($) { + my ($hash) = @_; + + my $url = "/withings"; + my $name = $data{FWEXT}{$url}{deviceName}; + $name = $hash->{NAME} if(!defined($name)); + Log3 $name, 2, "Disabling Withings webcall for $name "; + delete $data{FWEXT}{$url}; + delete $modules{"livetracking"}{defptr}{"webcall"}; +} + +######################### +sub withings_Webcall() { + my ($request) = @_; + + Log3 "withings", 4, "Withings webcall: ".$request; + + my $hash = $modules{"withings"}{defptr}{"webcall"}; + + if(!defined($hash)){ + Log3 "withings", 1, "Withings webcall hash not defined!"; + return ( "text/plain; charset=utf-8", + "undefined" ); + } + my $name = $hash->{NAME}; + + if($request =~ /state=connect/){ + $request =~ /code=(.*?)(&|$)/; + my $code = $1 || undef; + Log3 "withings", 2, "Withings webcall code ".$code; + withings_AuthApp($hash,$code); + return ( "text/plain; charset=utf-8", + "You can close this window now." ); + } else { + Log3 "withings", 1, "Withings webcall: ".$request; + } + if($request =~ /userid=/){ + $request =~ /userid=(.*?)(&|$)/; + my $userid = $1 || undef; + if(!defined($userid)){ + Log3 "withings", 1, "Withings webcall userid missing ".$request; + return ( "text/plain; charset=utf-8", + "1" ); + } + my $userhash = $modules{$hash->{TYPE}}{defptr}{"U$userid"}; + if(!defined($userhash)){ + Log3 "withings", 1, "Withings webcall user missing ".$request; + return ( "text/plain; charset=utf-8", + "1" ); + } + InternalTimer(gettimeofday()+2, "withings_poll", $userhash, 0); + + return ( "text/plain; charset=utf-8", + "0" ); + } + return ( "text/plain; charset=utf-8", + "1" ); + + return undef; +} + +sub withings_AuthApp($;$) { + my ($hash,$code) = @_; + my $name = $hash->{NAME}; + + + # https://account.withings.com/oauth2/token [grant_type=authorization_code...] + # grant_type=authorization_code&client_id=[STRING]&client_secret=[STRING]&code=[STRING]&redirect_uri=[STRING] + + my $cid = AttrVal($name,'client_id',''); + my $cb = AttrVal($name,'callback_url',''); + + my $url = "https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=".$cid."&scope=user.info,user.metrics,user.activity&state=connect&redirect_uri=".$cb; + return $url if(!defined($code) || $code eq ""); + + my $cs = AttrVal($name,'client_secret',''); + + Log3 "withings", 2, "Withings auth call ".$code; + + my $datahash = { + url => "https://account.withings.com/oauth2/token", + method => "POST", + timeout => 10, + noshutdown => 1, + data => { grant_type => 'authorization_code', client_id => $cid, client_secret => $cs, code => $code, redirect_uri => $cb }, + }; + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 $name, 1, "$name: LOGIN ERROR: ".Dumper($err); + return undef; + } + #Log3 $name, 1, "$name: LOGIN SUCCESS ".Dumper($data); + + my $json = eval { JSON::decode_json($data) }; + if($@) + { + Log3 $name, 1, "$name: LOGIN JSON ERROR: $data"; + return undef; + } + if(defined($json->{errors})){ + Log3 $name, 2, "$name: LOGIN RETURN ERROR: $data"; + return undef; + } + + Log3 $name, 4, "$name: LOGIN SUCCESS: $data"; + + my $user = $json->{userid} || "NOUSER"; + my $userhash = $modules{$hash->{TYPE}}{defptr}{"U$user"}; + if(!defined($userhash)){ + Log3 $name, 2, "$name: LOGIN USER ERROR: $data"; + return undef; + } + #readingsSingleUpdate( $hash, "access_token", $json->{access_token}, 1 ) if(defined($json->{access_token})); + $userhash->{helper}{OAuthKey} = $json->{access_token} if(defined($json->{access_token})); + #readingsSingleUpdate( $hash, "expires_in", $json->{expires_in}, 1 ) if(defined($json->{expires_in})); + $userhash->{helper}{OAuthValid} = (int(time)+$json->{expires_in}) if(defined($json->{expires_in})); + readingsSingleUpdate( $userhash, ".refresh_token", $json->{refresh_token}, 1 ) if(defined($json->{refresh_token})); + + InternalTimer(gettimeofday()+$json->{expires_in}, "withings_AuthRefresh", $userhash, 0); + + + #https://wbsapi.withings.net/notify?action=subscribe&access_token=a639e912dfc31a02cc01ea4f38de7fa4a1464c2e&callbackurl=http://fhem:remote@gu9mohkaxqdgpix5.myfritz.net/fhem/withings&appli=1&comment=fhem + + return undef; +} + + +sub withings_AuthRefresh($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $cid = AttrVal($hash->{IODev}->{NAME},'client_id',''); + my $cs = AttrVal($hash->{IODev}->{NAME},'client_secret',''); + my $ref = ReadingsVal($name,'.refresh_token',''); + + my $datahash = { + url => "https://account.withings.com/oauth2/token", + method => "POST", + timeout => 10, + noshutdown => 1, + data => { grant_type => 'refresh_token', client_id => $cid, client_secret => $cs, refresh_token => $ref }, + }; + + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 $name, 1, "$name: REFRESH ERROR $err"; + return undef; + } + + my $json = eval { JSON::decode_json($data) }; + if($@) + { + Log3 $name, 1, "$name: REFRESH JSON ERROR: $data"; + return undef; + } + if(defined($json->{errors})){ + Log3 $name, 2, "$name: REFRESH RETURN ERROR: $data"; + return undef; + } + + Log3 $name, 4, "$name: REFRESH SUCCESS: $data"; + + #readingsSingleUpdate( $hash, "access_token", $json->{access_token}, 1 ) if(defined($json->{access_token})); + $hash->{helper}{OAuthKey} = $json->{access_token} if(defined($json->{access_token})); + #readingsSingleUpdate( $hash, "expires_in", $json->{expires_in}, 1 ) if(defined($json->{expires_in})); + $hash->{helper}{OAuthValid} = (int(time)+$json->{expires_in}) if(defined($json->{expires_in})); + readingsSingleUpdate( $hash, ".refresh_token", $json->{refresh_token}, 1 ) if(defined($json->{refresh_token})); + + InternalTimer(gettimeofday()+$json->{expires_in}, "withings_AuthRefresh", $hash, 0); + + #https://wbsapi.withings.net/notify?action=subscribe&access_token=a639e912dfc31a02cc01ea4f38de7fa4a1464c2e&callbackurl=http://fhem:remote@gu9mohkaxqdgpix5.myfritz.net/fhem/withings&appli=1&comment=fhem + + return undef; +} + +sub withings_AuthList($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $acc = $hash->{helper}{OAuthKey}; + + my $datahash = { + url => "https://wbsapi.withings.net/notify", + method => "GET", + timeout => 10, + noshutdown => 1, + data => { action => 'list', access_token => $acc }, + }; + + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 $name, 1, "$name: LIST ERROR $err"; + return undef; + } + + my $json = eval { JSON::decode_json($data) }; + if($@) + { + Log3 $name, 1, "$name: LIST JSON ERROR: $data"; + return undef; + } + if(defined($json->{errors})){ + Log3 $name, 2, "$name: LIST RETURN ERROR: $data"; + return undef; + } + + my $ret = ""; + foreach my $profile (@{$json->{body}{profiles}}) { + next if( !defined($profile->{appli}) ); + $ret .= $profile->{appli}; + $ret .= "\t"; + $ret .= $profile->{comment}; + $ret .= "\t"; + $ret .= $profile->{callbackurl}; + $ret .= "\n"; + } + return "No subscriptions found!" if($ret eq ""); + return $ret; + +} + +sub withings_AuthUnsubscribe($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $acc = $hash->{helper}{OAuthKey}; + my $cb = AttrVal($hash->{IODev}->{NAME},'callback_url',''); + + my @applis = ("1", "4", "16", "44", "46"); + foreach my $appli (@applis) { + + my $datahash = { + url => "https://wbsapi.withings.net/notify", + method => "GET", + timeout => 10, + noshutdown => 1, + data => { action => 'revoke', access_token => $acc, callbackurl => $cb, appli => $appli }, + }; + + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 $name, 1, "$name: REVOKE ERROR $err"; + #return undef; + } + + my $json = eval { JSON::decode_json($data) }; + if($@) + { + Log3 $name, 1, "$name: REVOKE JSON ERROR: $data"; + #return undef; + } + if(defined($json->{error})){ + Log3 $name, 2, "$name: REVOKE RETURN ERROR: $data"; + #return undef; + } + + next if($json->{status} == 0); + Log3 $name, 1, "$name: REVOKE PROBLEM: $data"; + + } + + + return undef; +} + +sub withings_AuthSubscribe($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $acc = $hash->{helper}{OAuthKey}; + my $cb = AttrVal($hash->{IODev}->{NAME},'callback_url',''); + my @applis = ("1", "4", "16", "44", "46"); + + my $ret = "Please open the following URLs in your browser to subscribe:\n\n"; + foreach my $appli (@applis) { + + $ret.='https://wbsapi.withings.net/notify?action=subscribe&access_token='.$acc.'&appli='.$appli.'&comment=FHEM&callbackurl='.$cb; + $ret .= "\n"; + next; + + my $ua = LWP::UserAgent->new; + my $request = HTTP::Request->new(GET => 'https://wbsapi.withings.net/notify?action=subscribe&access_token='.$acc.'&appli='.$appli.'&comment=FHEM&callbackurl='.$cb); + my $response = $ua->request($request); + Log3 $name, 2, "$name: SUBSCRIBE ".Dumper($response->content); + + next; + + my $datahash = { + url => "https://wbsapi.withings.net/notify", + method => "GET", + timeout => 10, + data => { action => 'subscribe', access_token => $acc, appli => $appli, comment => 'FHEM', callbackurl => $cb }, + }; + + my($err,$data) = HttpUtils_BlockingGet($datahash); + + #Log3 $name, 1, "$name: SUBSCRIBE ".Dumper($datahash); + + if ($err || !defined($data) || $data =~ /Authentification failed/ || $data =~ /not a valid/) + { + Log3 $name, 1, "$name: SUBSCRIBE ERROR $err"; + return undef; + } + + my $json = eval { JSON::decode_json($data) }; + if($@) + { + Log3 $name, 1, "$name: SUBSCRIBE JSON ERROR: $data"; + return undef; + } + if(defined($json->{error})){ + Log3 $name, 2, "$name: SUBSCRIBE RETURN ERROR: $data"; + return undef; + } + + #next if($json->{status} == 0); + Log3 $name, 2, "$name: SUBSCRIBE SUCCESS: $data"; + + } + return $ret; + + return undef; +} + ########################## sub withings_DbLog_splitFn($) { @@ -3321,17 +3813,17 @@ sub withings_DbLog_splitFn($) { elsif($event =~ m/temperature/) { $reading = 'temperature'; - $unit = 'ËšC'; + $unit = '°C'; } elsif($event =~ m/bodyTemperature/) { $reading = 'bodyTemperature'; - $unit = 'ËšC'; + $unit = '°C'; } elsif($event =~ m/skinTemperature/) { $reading = 'skinTemperature'; - $unit = 'ËšC'; + $unit = '°C'; } elsif($event =~ m/humidity/) { @@ -3433,6 +3925,26 @@ sub withings_DbLog_splitFn($) { $reading = 'batteryPercent'; $unit = '%'; } + elsif($event =~ m/durationTo/) + { + $value = $parts[1]; + $unit = 's'; + } + elsif($event =~ m/Duration/) + { + $value = $parts[1]; + $unit = 's'; + } + elsif($event =~ m/heartrate/) + { + $value = $parts[1]; + $unit = 'bpm'; + } + elsif($event =~ m/pressure/) + { + $value = $parts[1]; + $unit = 'mmHg'; + } else { $value = $parts[1]; @@ -3639,7 +4151,11 @@ sub withings_weekdays2Int( $ ) {
  • sleepDurationLight
  • sleepDurationDeep
  • sleepDurationREM
  • +
  • sleepDurationTotal
  • wakeupCount
  • +
  • snoringDuration
  • +
  • snoringEpisodeCount
  • +
  • sleepScore
  • co2
  • temperature
  • diff --git a/fhem/FHEM/38_netatmo.pm b/fhem/FHEM/38_netatmo.pm index 9cfe0dfb0..1087f766b 100644 --- a/fhem/FHEM/38_netatmo.pm +++ b/fhem/FHEM/38_netatmo.pm @@ -3,7 +3,7 @@ # # 38_netatmo.pm # -# 2018 Markus Moises < vorname at nachname . de > +# 2019 Markus Moises < vorname at nachname . de > # # Based on original code by justme1968 # @@ -11,7 +11,7 @@ # # ############################################################################## -# Release 23 / 2018-11-03 +# Release 24 / 2019-01-05 package main; @@ -592,6 +592,7 @@ netatmo_Set($$@) $list = "clear:noArg webhook:add,drop" if ($hash->{SUBTYPE} eq "WEBHOOK"); return undef if( $list eq "" ); + $cmd = "(undefined)" if(!defined($cmd)); if( $cmd eq "autocreate" ) { return netatmo_autocreate($hash, 1 ); @@ -6486,8 +6487,8 @@ sub netatmo_weatherIcon() Webhook

    @@ -6563,21 +6564,21 @@ sub netatmo_weatherIcon() Attributes diff --git a/fhem/FHEM/60_allergy.pm b/fhem/FHEM/60_allergy.pm index 1f267e685..48b28dd1a 100755 --- a/fhem/FHEM/60_allergy.pm +++ b/fhem/FHEM/60_allergy.pm @@ -122,7 +122,7 @@ sub allergy_Define($$$) { return undef; } - $hash->{STATE} = "Initialized"; + #$hash->{STATE} = "Initialized"; return undef; } diff --git a/fhem/FHEM/72_XiaomiDevice.pm b/fhem/FHEM/72_XiaomiDevice.pm index 4d899d7d6..2861d1a61 100755 --- a/fhem/FHEM/72_XiaomiDevice.pm +++ b/fhem/FHEM/72_XiaomiDevice.pm @@ -3,7 +3,7 @@ # # 72_XiaomiDevice.pm # -# 2018 Markus Moises < vorname at nachname . de > +# 2019 Markus Moises < vorname at nachname . de > # # This module connects to Xiaomi Smart Home WiFi devices # Currently supported: Air Purifier, Robot Vacuum, Smart Fan, UV Humidifier, Lamps, Rice Cooker, Power Plugs @@ -511,6 +511,11 @@ sub XiaomiDevice_Get($@) { # #zone {"from":"4","id":1164,"method":"app_zoned_clean","params":[[19500,22700,21750,24250,3],[23150,26050,25150,27500,3],[23650,22950,25150,26250,3],[21700,23000,23750,24150,3],[23700,23050,25200,24200,3]]} #goto {"from":"4","id":1293,"method":"app_goto_target","params":[21500,25250]} +#save_map +#new status {"id":927,"method":"get_prop","params":["get_status"]} +#{"id":8103,"method":"get_log_upload_status"} +#{"id":8034,"method":"start_edit_map"} +#{"id":8054,"method":"save_map","params":[[1,26353,26920,27314,26042],[0,25375,26490,25884,26490,25884,25860,25375,25860]]} ##################################### @@ -575,6 +580,7 @@ sub XiaomiDevice_Set($$@) { $list .= ' start:noArg stop:noArg pause:noArg spot:noArg charge:noArg locate:noArg dnd_enabled:on,off dnd_start dnd_end move remotecontrol:start,stop,forward,left,right reset_consumable:filter,mainbrush,sidebrush,sensors timezone volume:slider,0,1,100 volume_test:noArg'; $list .= ' carpet_mode:on,off'; $list .= ' sleep:noArg wakeup:noArg'; + $list .= ' save_map'; $list .= ' fan_power:slider,1,1,100' if(defined($hash->{model}) && $hash->{model} eq "rockrobo.vacuum.v1"); $list .= ' cleaning_mode:quiet,balanced,turbo,max,mop'; @@ -916,6 +922,13 @@ sub XiaomiDevice_Set($$@) { $hash->{helper}{packet}{$packetid} = "app_sleep"; XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"app_sleep","params":[]}' ); } + elsif ($cmd eq 'save_map') + { + my $packetid = $hash->{helper}{packetid}; + $hash->{helper}{packetid} = $packetid+1; + $hash->{helper}{packet}{$packetid} = "save_map"; + XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"save_map","params":['.$arg[0].']}' ); + } elsif ($cmd eq 'timezone') { my $timezone = join(" ", @arg); @@ -1614,12 +1627,12 @@ sub XiaomiDevice_GetUpdate($) elsif( defined($attr{$name}) && defined($attr{$name}{subType}) && $attr{$name}{subType} eq "WaterPurifier") { $hash->{helper}{packet}{$packetid} = "water_data"; - XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"get_prop","params":["power","mode","tds","filter1_life","filter1_state","filter_life","filter_state","life","state","level","volume","filter","usage","temperature","uv_life","uv_state","elecval_state","button_pressed"]}' ); + XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"get_prop","params":["power","mode","tds_out","filter1_life","filter1_state","filter_life","filter_state","life","state","level","volume","filter","usage","temperature","uv_life","uv_state","elecval_state","button_pressed"]}' ); } elsif( defined($attr{$name}) && defined($attr{$name}{subType}) && $attr{$name}{subType} eq "Camera") { $hash->{helper}{packet}{$packetid} = "camera_data"; - XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"get_prop","params":["auto_low_light"]}' ); + XiaomiDevice_WriteJSON($hash, '{"id":'.$packetid.',"method":"get_power","params":[]}' ); } elsif( defined($attr{$name}) && defined($attr{$name}{subType}) && $attr{$name}{subType} eq "RiceCooker") { @@ -1960,6 +1973,7 @@ sub XiaomiDevice_ParseJSON($$) return undef if($msgtype eq "test_sound_volume"); return undef if($msgtype eq "app_wakeup_robot"); return undef if($msgtype eq "app_sleep"); + return undef if($msgtype eq "save_map"); if($msgtype eq "air_data") { @@ -2298,6 +2312,7 @@ sub XiaomiDevice_ParseJSON($$) } #{ "result": [ { "msg_ver": 3, "msg_seq": 4, "state": 8, "battery": 100, "clean_time": 3, "clean_area": 0, "error_code": 0, "map_present": 0, "in_cleaning": 0, "fan_power": 10, "dnd_enabled": 1 } ], "id": 1201 } + #{"id":8493504,"result":[{"msg_ver":2,"msg_seq":13,"state":8,"battery":100,"clean_time":338,"clean_area":2520000,"error_code":0,"map_present":1,"in_cleaning":0,"in_returning":0,"in_fresh_state":1,"lab_status":1,"fan_power":60,"dnd_enabled":0}]} if($msgtype eq "get_status") { return undef if(!defined($json->{result})); @@ -2335,6 +2350,9 @@ sub XiaomiDevice_ParseJSON($$) readingsBulkUpdate( $hash, "error_code", $vacuum_errors{$json->{result}[0]{error_code}}, 1 ) if(defined($json->{result}[0]{error_code})); readingsBulkUpdate( $hash, "map_present", (($json->{result}[0]{map_present} eq "1")?"yes":"no"), 1 ) if(defined($json->{result}[0]{map_present})); readingsBulkUpdate( $hash, "in_cleaning", (($json->{result}[0]{in_cleaning} eq "1")?"yes":"no"), 1 ) if(defined($json->{result}[0]{in_cleaning})); #not working or used for something else + readingsBulkUpdate( $hash, "in_returning", (($json->{result}[0]{in_returning} eq "1")?"yes":"no"), 1 ) if(defined($json->{result}[0]{in_returning})); + readingsBulkUpdate( $hash, "in_fresh_state", (($json->{result}[0]{in_fresh_state} eq "1")?"yes":"no"), 1 ) if(defined($json->{result}[0]{in_fresh_state})); + readingsBulkUpdate( $hash, "lab_status", (($json->{result}[0]{lab_status} eq "1")?"yes":"no"), 1 ) if(defined($json->{result}[0]{lab_status})); readingsBulkUpdate( $hash, "fan_power", $json->{result}[0]{fan_power}, 1 ) if(defined($json->{result}[0]{fan_power})); readingsBulkUpdate( $hash, "dnd", (($json->{result}[0]{dnd_enabled} eq "1")?"on":"off"), 1 ) if(defined($json->{result}[0]{dnd_enabled})); if(defined($json->{result}[0]{fan_power}) && int($json->{result}[0]{fan_power}) > 100) { @@ -2343,7 +2361,7 @@ sub XiaomiDevice_ParseJSON($$) readingsBulkUpdate( $hash, "cleaning_mode", $cleaningmode, 1 ); } elsif(defined($json->{result}[0]{fan_power})) { my $cleaning_int = int($json->{result}[0]{fan_power}); - my $cleaningmode = ($cleaning_int > 89) ? "max" : ($cleaning_int > 75) ? "turbo" : ($cleaning_int > 40) ? "balanced" : ($cleaning_int > 10) ? "quiet" : "mop"; + my $cleaningmode = ($cleaning_int > 89) ? "max" : ($cleaning_int > 74) ? "turbo" : ($cleaning_int > 40) ? "balanced" : ($cleaning_int > 10) ? "quiet" : "mop"; readingsBulkUpdate( $hash, "cleaning_mode", $cleaningmode, 1 ); } readingsEndUpdate($hash,1); @@ -2423,7 +2441,7 @@ sub XiaomiDevice_ParseJSON($$) readingsSingleUpdate( $hash, "cleaning_mode", $cleaningmode, 1 ); } elsif(defined($json->{result}[0])) { my $cleaning_int = int($json->{result}[0]); - my $cleaningmode = ($cleaning_int > 89) ? "max" : ($cleaning_int > 75) ? "turbo" : ($cleaning_int > 40) ? "balanced" : ($cleaning_int > 10) ? "quiet" : "mop"; + my $cleaningmode = ($cleaning_int > 89) ? "max" : ($cleaning_int > 74) ? "turbo" : ($cleaning_int > 40) ? "balanced" : ($cleaning_int > 10) ? "quiet" : "mop"; readingsSingleUpdate( $hash, "cleaning_mode", $cleaningmode, 1 ); } return undef; @@ -2990,7 +3008,15 @@ sub XiaomiDevice_Write($$) { # Send successful Log3 $hash, 5, "$name Send SUCCESS"; - InternalTimer(gettimeofday() + 30, "XiaomiDevice_connectFail", $hash, 0) if(length($msg) > 40); + if(length($msg) > 40){ + my $currentstate = ReadingsVal($name,"state","-"); + if(lc($currentstate) =~ /clean/ || lc($currentstate) =~ /goto/){ + InternalTimer(gettimeofday() + 150, "XiaomiDevice_connectFail", $hash, 0); + #Log3 $hash, 5, "$name: cleaning, higher timeout"; + } else { + InternalTimer(gettimeofday() + 45, "XiaomiDevice_connectFail", $hash, 0); + } + } } Log3 $hash, 5, "$name > ".unpack('H*',$msg); diff --git a/fhem/FHEM/98_livetracking.pm b/fhem/FHEM/98_livetracking.pm index ff3b400cb..c445b14d8 100644 --- a/fhem/FHEM/98_livetracking.pm +++ b/fhem/FHEM/98_livetracking.pm @@ -3,14 +3,14 @@ # # 98_livetracking.pm # -# 2018 Markus Moises < vorname at nachname . de > +# 2019 Markus Moises < vorname at nachname . de > # -# This module provides livetracking data from OwnTracks, OpenPaths and Swarm (FourSquare) +# This module provides livetracking data from OwnTracks, OpenPaths, Life360 and Swarm (FourSquare) # # ############################################################################## # -# define livetracking +# define livetracking # ############################################################################## @@ -84,6 +84,8 @@ sub livetracking_Initialize($) { "addressReading:0,1 ". "osmandServer:0,1 ". "osmandId ". + "life360_userid ". + "life360_circle ". $readingFnAttributes; @@ -93,23 +95,40 @@ sub livetracking_Define($$$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); - return "syntax: define livetracking " if(int(@a) < 2 || int(@a) > 7 ); + return "syntax: define livetracking " if(int(@a) < 2 || int(@a) > 7 ); my $name = $hash->{NAME}; #$hash->{OAuth_exists} = $libcheck_hasOAuth if($libcheck_hasOAuth); if(int(@a) == 4 ) { - $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists}); - $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists}); + if ($a[2] =~ /@/) { + $hash->{helper}{life360_user} = $a[2]; + $hash->{helper}{life360_pass} = $a[3]; + } else { + $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists}); + $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists}); + } } elsif(int(@a) == 3 ) { $hash->{helper}{swarm_token} = $a[2]; } elsif(int(@a) == 5 ) { - $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists}); - $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists}); + if ($a[2] =~ /@/) { + $hash->{helper}{life360_user} = $a[2]; + $hash->{helper}{life360_pass} = $a[3]; + } else { + $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists}); + $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists}); + } $hash->{helper}{swarm_token} = $a[4]; } + elsif(int(@a) == 7 ) { + $hash->{helper}{life360_user} = $a[2]; + $hash->{helper}{life360_pass} = $a[3]; + $hash->{helper}{openpaths_key} = $a[4];# if($hash->{OAuth_exists}); + $hash->{helper}{openpaths_secret} = $a[5];# if($hash->{OAuth_exists}); + $hash->{helper}{swarm_token} = $a[6]; + } my $req = eval @@ -157,6 +176,11 @@ sub livetracking_Define($$$) { $attr{$name}{stateFormat} = 'location'; } + livetracking_BootstrapLife360($hash) if(defined($hash->{helper}{life360_user})); + + + livetracking_addExtension($hash) if(AttrVal($name, "osmandServer", 0) == 1); + #$hash->{STATE} = "Initialized"; return undef; @@ -164,7 +188,9 @@ sub livetracking_Define($$$) { sub livetracking_Undefine($$) { my ($hash, $arg) = @_; + my $name = $hash->{NAME}; RemoveInternalTimer($hash); + livetracking_removeExtension($hash) if(AttrVal($name, "osmandServer", 0) == 1); return undef; } @@ -177,7 +203,13 @@ sub livetracking_Set($$@) { { $usage .= " owntracksMessage"; } - else{ + if(defined($hash->{helper}{life360_user})) + { + $usage .= " BootstrapLife360:noArg"; + } + + if(!defined($hash->{helper}{life360_user}) && !defined($attr{$name}{owntracksDevice})) + { $usage = undef; } @@ -201,6 +233,13 @@ sub livetracking_Set($$@) { fhem('set '.$devname.' cmd {"_type":"cmd","action":"action",'.$messagetext.$notifytext.'"tst":'.time().'}'); #fhem('set '.$devname.' msg {"_type":"cmd","action":"notify", "content":"'.$notifytext.'","tst":'.time().'}') if($notifytext ne ""); } + elsif($command eq "BootstrapLife360") + { + $hash->{helper}{life360_script} = ""; + $hash->{helper}{life360_secret} = ""; + $hash->{helper}{life360_token} = ""; + livetracking_BootstrapLife360($hash); + } return undef; } @@ -212,9 +251,12 @@ sub livetracking_Get($@) { my $name = $hash->{NAME}; - my $usage = "Unknown argument $command, choose one of All:noArg OpenPaths:noArg Swarm:noArg"; + my $usage = "Unknown argument $command, choose one of All:noArg"; + $usage .= " OpenPaths:noArg" if(defined($hash->{helper}{openpaths_key})); + $usage .= " Swarm:noArg" if(defined($hash->{helper}{swarm_token})); $usage .= " owntracksLocation:noArg owntracksSteps:noArg" if(defined($attr{$name}{owntracksDevice})); $usage .= " address"; + $usage .= " Life360:noArg" if(defined($hash->{helper}{life360_user})); return $usage if $command eq '?'; @@ -269,29 +311,40 @@ sub livetracking_Get($@) { if($parameter eq "long"){ $addr .= $json->{address}->{housename}."\n" if(defined($json->{address}->{housename})); $addr .= $json->{address}->{parking}."\n" if(defined($json->{address}->{parking})); + $addr .= $json->{address}->{locality}."\n" if(defined($json->{address}->{locality})); } $addr .= $json->{address}->{road}." " if(defined($json->{address}->{road})); $addr .= $json->{address}->{path}." " if(defined($json->{address}->{path}) && !defined($json->{address}->{road})); $addr .= $json->{address}->{bridleway}." " if(defined($json->{address}->{bridleway}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path})); $addr .= $json->{address}->{footway}." " if(defined($json->{address}->{footway}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway})); - $addr .= $json->{address}->{neighbourhood}." " if(defined($json->{address}->{neighbourhood}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway})); + $addr .= $json->{address}->{square}." " if(defined($json->{address}->{square}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway})); + $addr .= $json->{address}->{neighbourhood}." " if(defined($json->{address}->{neighbourhood}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway}) && !defined($json->{address}->{square})); + $addr .= $json->{address}->{city_block}." " if(defined($json->{address}->{city_block}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway}) && !defined($json->{address}->{square}) && !defined($json->{address}->{neighbourhood})); + $addr .= $json->{address}->{hamlet}." " if(defined($json->{address}->{hamlet}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway}) && !defined($json->{address}->{square}) && !defined($json->{address}->{neighbourhood}) && !defined($json->{address}->{city_block})); + $addr .= $json->{address}->{isolated_dwelling}." " if(defined($json->{address}->{isolated_dwelling}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway}) && !defined($json->{address}->{square}) && !defined($json->{address}->{neighbourhood}) && !defined($json->{address}->{city_block}) && !defined($json->{address}->{hamlet})); + $addr .= $json->{address}->{farm}." " if(defined($json->{address}->{farm}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}) && !defined($json->{address}->{footway}) && !defined($json->{address}->{square}) && !defined($json->{address}->{neighbourhood}) && !defined($json->{address}->{city_block}) && !defined($json->{address}->{hamlet}) && !defined($json->{address}->{isolated_dwelling})); $addr .= $json->{address}->{house_number} if(defined($json->{address}->{house_number})); #$addr .= "\n".$json->{address}->{neighbourhood} if(defined($json->{address}->{neighbourhood}) && $parameter eq "long"); - if($parameter eq "long"){ - $addr .= "\n".$json->{address}->{suburb} if(defined($json->{address}->{suburb})); - } - $addr .= "\n" if(defined($json->{address}->{postcode}) || defined($json->{address}->{city}) || defined($json->{address}->{town}) || defined($json->{address}->{village})); + #if($parameter eq "long"){ + # $addr .= "\n".$json->{address}->{suburb} if(defined($json->{address}->{suburb})); + #} + $addr .= (($parameter eq "singleline")?", ":"\n") if(defined($json->{address}->{postcode}) || defined($json->{address}->{city}) || defined($json->{address}->{town}) || defined($json->{address}->{village}) || defined($json->{address}->{hamlet}) || defined($json->{address}->{suburb})); $addr .= $json->{address}->{postcode}." " if(defined($json->{address}->{postcode})); $addr .= $json->{address}->{city} if(defined($json->{address}->{city})); $addr .= $json->{address}->{town}." " if(defined($json->{address}->{town}) && !defined($json->{address}->{city})); $addr .= $json->{address}->{village}." " if(defined($json->{address}->{village}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town})); + $addr .= $json->{address}->{borough}." " if(defined($json->{address}->{borough}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}) && !defined($json->{address}->{village})); + $addr .= $json->{address}->{suburb}." " if(defined($json->{address}->{suburb}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}) && !defined($json->{address}->{village}) && !defined($json->{address}->{borough})); + $addr .= $json->{address}->{quarter}." " if(defined($json->{address}->{quarter}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}) && !defined($json->{address}->{village}) && !defined($json->{address}->{borough}) && !defined($json->{address}->{suburb})); + $addr .= $json->{address}->{municipality}." " if(defined($json->{address}->{municipality}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}) && !defined($json->{address}->{village}) && !defined($json->{address}->{borough}) && !defined($json->{address}->{suburb}) && !defined($json->{address}->{quarter})); + $addr .= $json->{address}->{hamlet}." " if(defined($json->{address}->{hamlet}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}) && !defined($json->{address}->{village}) && !defined($json->{address}->{borough}) && !defined($json->{address}->{suburb}) && !defined($json->{address}->{quarter}) && !defined($json->{address}->{municipality})); if($parameter eq "long"){ $addr .= "\n".$json->{address}->{county} if(defined($json->{address}->{county})); $addr .= "\n" if((defined($json->{address}->{state_district}) || defined($json->{address}->{state}))); $addr .= $json->{address}->{state_district}." " if(defined($json->{address}->{state_district})); $addr .= $json->{address}->{state} if(defined($json->{address}->{state})); } - $addr .= "\n".$json->{address}->{country} if(defined($json->{address}->{country})); + $addr .= (($parameter eq "singleline")?", ":"\n").$json->{address}->{country} if(defined($json->{address}->{country})); Log3 ($name, 4, "$name: address received\n".Dumper($json)); readingsSingleUpdate($hash,"address",livetracking_utf8clean($addr),1) if(AttrVal($name,"addressReading",0)); return livetracking_utf8clean($addr); @@ -308,6 +361,12 @@ sub livetracking_Get($@) { } + elsif($command eq "Life360") + { + livetracking_GetLife360($hash); + } + + return undef; } @@ -374,6 +433,60 @@ sub livetracking_GetAll($) { InternalTimer( gettimeofday() + 10, "livetracking_GetOpenPaths", $hash, 0) if(defined($hash->{helper}{openpaths_key})); + + InternalTimer( gettimeofday() + 20, "livetracking_GetLife360", $hash, 0) if(defined($hash->{helper}{life360_user})); + + return undef; +} + + + +sub livetracking_GetLife360($) { + + my ($hash) = @_; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash, "livetracking_GetLife360"); + + if(IsDisabled($name)) + { + Log3 ($name, 4, "livetracking $name is disabled, data update cancelled."); + return undef; + } + + if(!defined($hash->{helper}{life360_user})) + { + return undef; + } + + if(!defined($hash->{helper}{life360_token}) or $hash->{helper}{life360_token} eq "") + { + livetracking_BootstrapLife360($hash); + return undef; + } + + my $lastupdate = ReadingsVal($name,".lastLife360",time()-3600); + $lastupdate = (time()-3600*6) if($lastupdate < (time()-3600*6)); + my $circle = $attr{$name}{life360_circle}; + my $userid = $attr{$name}{life360_userid}; + + my $url = "https://www.life360.com/v3/circles/".$circle."/members/".$userid."/history?time=".int($lastupdate); + + HttpUtils_NonblockingGet({ + url => $url, + header => "Authorization: Bearer ".$hash->{helper}{life360_token}, + noshutdown => 1, + hash => $hash, + type => 'life360data', + callback => \&livetracking_dispatch, + }); + + + my $interval = AttrVal($hash->{NAME}, "interval", 1800); + #RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + $interval, "livetracking_GetLife360", $hash, 0); + $hash->{UPDATED} = FmtDateTime(time()); + return undef; } @@ -476,7 +589,7 @@ sub livetracking_GetSwarm($) { }); - my $interval = AttrVal($hash->{NAME}, "interval", 1800); + my $interval = AttrVal($hash->{NAME}, "interval", 900); #RemoveInternalTimer($hash); InternalTimer( gettimeofday() + $interval, "livetracking_GetSwarm", $hash, 0); $hash->{UPDATED} = FmtDateTime(time()); @@ -486,6 +599,127 @@ sub livetracking_GetSwarm($) { +sub livetracking_ParseLife360($$) { + my ($hash,$json) = @_; + my $name = $hash->{NAME}; + + my $updated = 0; + + my $lastreading = ReadingsVal($name,".lastLife360",time()-300); + + Log3 ($name, 5, "$name Life360 data: /n".Dumper($json)); + + my $battery = -1; + my $charge = -1; + my $tst = int(time); + + foreach my $dataset (reverse(@{$json->{locations}})) + { + next if(!defined($dataset->{latitude})); + + if(defined($dataset->{battery}) && defined($dataset->{endTimestamp})) + { + $battery = $dataset->{battery}; + $charge = $dataset->{charge}; + $tst = $dataset->{endTimestamp}; + } + + next if($lastreading > $dataset->{startTimestamp}); + + Log3 ($name, 2, "$name new l360 data: /n".Dumper($dataset)); + + my $accurate = 1; + $accurate = 0 if(defined($attr{$name}{filterAccuracy}) and defined($dataset->{accuracy}) and $attr{$name}{filterAccuracy} < $dataset->{accuracy}); + + Log3 ($name, 5, "$name Life360: ".$dataset->{latitude}.",".$dataset->{longitude}); + + $lastreading = $dataset->{endTimestamp}+1; + + readingsBeginUpdate($hash); # Begin update readings + $hash->{".updateTimestamp"} = FmtDateTime($dataset->{endTimestamp}); + my $changeindex = 0; + + + if($accurate){ + readingsBulkUpdate($hash, "latitude", $dataset->{latitude}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + readingsBulkUpdate($hash, "longitude", $dataset->{longitude}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + readingsBulkUpdate($hash, "location", $dataset->{latitude}.",".$dataset->{longitude}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + + if(defined($dataset->{speed}) and $dataset->{speed} >= 0 and $accurate) + { + readingsBulkUpdate($hash, "velocity", $dataset->{speed}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + + readingsBulkUpdate($hash, "accuracy", $dataset->{accuracy}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + + + if(defined($dataset->{name}) and $dataset->{name} ne "") + { + readingsBulkUpdate($hash, "place", $dataset->{name}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + elsif(defined($dataset->{shortAddress}) and $dataset->{shortAddress} ne "") + { + readingsBulkUpdate($hash, "place", $dataset->{shortAddress}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + elsif(defined($dataset->{address1}) and $dataset->{address1} ne "") + { + readingsBulkUpdate($hash, "place", $dataset->{address1}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + + if(defined($attr{$name}{home}) && $accurate) + { + readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{latitude}.",".$dataset->{longitude},$attr{$name}{home})); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + + if(defined($dataset->{battery})) + { + readingsBulkUpdate($hash, "batteryPercent", $dataset->{battery}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + readingsBulkUpdate($hash, "batteryState", (int($dataset->{battery}) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok"); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + readingsBulkUpdate($hash, "batteryCharge", ($charge == -1)?"unknown":($charge == 1)?"charge":"discharge"); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); + } + + $updated = 1; + + readingsEndUpdate($hash, 1); + + } + + if($battery >= 0 && $updated == 0) + { + readingsBeginUpdate($hash); + $hash->{".updateTimestamp"} = FmtDateTime($tst); + readingsBulkUpdate($hash, "batteryPercent", $battery); + $hash->{CHANGETIME}[0] = FmtDateTime($tst); + readingsBulkUpdate($hash, "batteryState", (int($battery) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok"); + $hash->{CHANGETIME}[1] = FmtDateTime($tst); + readingsBulkUpdate($hash, "batteryCharge", ($charge == -1)?"unknown":($charge == 1)?"charge":"discharge"); + $hash->{CHANGETIME}[0] = FmtDateTime($tst); + readingsEndUpdate($hash, 1); + } + + if($updated == 1) + { + readingsSingleUpdate($hash,".lastLife360",$lastreading,1); + $hash->{helper}{lastLife360} = $lastreading; + } + + return undef; +} + + sub livetracking_ParseOpenPaths($$) { my ($hash,$json) = @_; my $name = $hash->{NAME}; @@ -513,13 +747,12 @@ sub livetracking_ParseOpenPaths($$) { my $changeindex = 0; - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); + readingsBulkUpdate($hash, "latitude", $dataset->{lat}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); + readingsBulkUpdate($hash, "longitude", $dataset->{lon}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); readingsBulkUpdate($hash, "location", $dataset->{lat}.",".$dataset->{lon}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "location", $dataset->{lat}.",".$dataset->{lon}, FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "location: ".$dataset->{lat}.",".$dataset->{lon}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); if(defined($dataset->{alt}) && $dataset->{alt} ne '0') @@ -529,59 +762,33 @@ sub livetracking_ParseOpenPaths($$) { if($altitude ne $newaltitude) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); readingsBulkUpdate($hash, "altitude", $newaltitude); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "altitude", $newaltitude." m", FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "altitude: ".$newaltitude." m"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); $altitude = $newaltitude; } } if(defined($dataset->{device}) && $dataset->{device} ne $device) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); readingsBulkUpdate($hash, "deviceOpenPaths", $dataset->{device}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "deviceOpenPaths", $dataset->{device}, FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "deviceOpenPaths: ".$dataset->{device}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); } if(defined($dataset->{os}) && $dataset->{os} ne $os) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); readingsBulkUpdate($hash, "osOpenPaths", $dataset->{os}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "osOpenPaths", $dataset->{os}, FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "osOpenPaths: ".$dataset->{os}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); } if(defined($dataset->{version}) && $dataset->{version} ne $version) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); readingsBulkUpdate($hash, "versionOpenPaths", $dataset->{version}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "versionOpenPaths", $dataset->{version}, FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "versionOpenPaths: ".$dataset->{version}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); } if(defined($attr{$name}{home})) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); - #setReadingsVal($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km", FmtDateTime($dataset->{t})); - #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t})); } $updated = 1; - #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); readingsEndUpdate($hash, 1); # End update readings } @@ -589,9 +796,6 @@ sub livetracking_ParseOpenPaths($$) { if($updated == 1) { - #readingsSingleUpdate($hash,"lastOpenPaths",$lastreading,1); - #$hash->{CHANGED} = (); - #$hash->{CHANGETIME} = (); readingsSingleUpdate($hash,".lastOpenPaths",$lastreading,1); $hash->{helper}{lastOpenPaths} = $lastreading; } @@ -638,42 +842,25 @@ sub livetracking_ParseSwarm($$) { $loc =~ s/$shl/$home/g; } - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); + readingsBulkUpdate($hash, "latitude", $dataset->{venue}->{location}->{lat}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); + readingsBulkUpdate($hash, "longitude", $dataset->{venue}->{location}->{lng}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); readingsBulkUpdate($hash, "location", $loc); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); - #setReadingsVal($hash, "location", $loc, FmtDateTime($dataset->{createdAt})); - #push(@{$hash->{CHANGED}}, "location: ".$loc); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); readingsBulkUpdate($hash, "place", $place); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); - #setReadingsVal($hash, "place", $dataset->{venue}->{name}, FmtDateTime($dataset->{createdAt})); - #push(@{$hash->{CHANGED}}, "place: ".$dataset->{venue}->{name}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); - if(defined($dataset->{source}->{name}) && $dataset->{source}->{name} ne $device) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); readingsBulkUpdate($hash, "deviceSwarm", $dataset->{source}->{name}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); - #setReadingsVal($hash, "deviceSwarm", $dataset->{source}->{name}, FmtDateTime($dataset->{createdAt})); - #push(@{$hash->{CHANGED}}, "deviceSwarm: ".$dataset->{source}->{name}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); } if(defined($attr{$name}{home})) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$loc,$attr{$name}{home})." km"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); - #setReadingsVal($hash, "distance", livetracking_distance($hash,$loc,$attr{$name}{home})." km", FmtDateTime($dataset->{createdAt})); - #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$loc,$attr{$name}{home})." km"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt})); } $updated = 1; @@ -695,46 +882,21 @@ sub livetracking_ParseSwarm($$) { } -sub livetracking_Notify($$) +sub livetracking_ParseOwnTracks { - my ($hash, $dev) = @_; + my ($hash,$data) = @_; my $name = $hash->{NAME}; - my $devName = $dev->{NAME}; - my $dataset = ""; - my $data = ""; - - # Ignore wrong notifications - if($devName eq AttrVal($name, "owntracksDevice" , "owntracks")) - { - Log3 ($name, 6, "$name OwnTracks data: /n".Dumper($dev)); - - my $invaliddata = 1; - if(($dev->{CHANGED}[0] =~ m/_type":[ ]?"location/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"position/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"transition/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"steps/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"beacon/ )) + my $dataset = eval { JSON->new->utf8(0)->decode($data) }; + if($@) { - $invaliddata = 0;#owntracks - } - elsif(($dev->{CHANGED}[0] =~ m/position":[ ]?{/)) - { - $invaliddata = 0;#traccar - } - if($invaliddata == 1){ - Log3 ($name, 5, "WRONG MQTT TYPE ".Dumper($dev->{CHANGED}[0])); - return undef; - } - - $data= substr($dev->{CHANGED}[0],index($dev->{CHANGED}[0], ": {")+2); - $dataset = JSON->new->utf8(0)->decode($data); - - } else { - Log3 ($name, 5, "livetracks: Notify ignored from ".$devName); + Log3 $name, 2, "$name: invalid json evaluation on ParseOwnTracks".Dumper($data); + #Log3 $name, 2, "$name: ".$param->{url}." / ".Dumper($data) if( $param->{type} eq 'life360data' ); return undef; } - - - if($dev->{CHANGED}[0] =~ m/_type":[ ]?"steps/) + if($data =~ m/_type":[ ]?"steps/) { readingsBeginUpdate($hash); # Start update readings $hash->{".updateTimestamp"} = FmtDateTime($dataset->{to}); @@ -752,7 +914,7 @@ sub livetracking_Notify($$) return undef; } - if($dev->{CHANGED}[0] =~ m/_type":[ ]?"beacon/) + if($data =~ m/_type":[ ]?"beacon/) { my $beaconid = $dataset->{uuid}.",".$dataset->{major}.",".$dataset->{minor}; @@ -784,6 +946,10 @@ sub livetracking_Notify($$) } + + #{"position":{"id":566,"attributes":{"batteryLevel":66,"distance":25.79,"totalDistance":20665.79,"motion":false},"deviceId":1,"type":null,"protocol":"osmand","serverTime":"2019-01-06T11:39:41.279+0000","deviceTime":"2019-01-06T11:39:41.000+0000","fixTime":"2019-01-06T11:39:41.000+0000","outdated":false,"valid":true,"latitude":53.xxxxx,"longitude":8.xxxxx,"altitude":0,"speed":0,"course":0,"address":null,"accuracy":24.04000091552734,"network":null},"device":{"id":1,"attributes":{},"groupId":0,"name":"SomeName","uniqueId":"SomeID,"status":"online","lastUpdate":"2019-01-06T11:39:41.279+0000","positionId":565,"geofenceIds":[],"phone":"","model":"","contact":"","category":null,"disabled":false}} + + my $accurate = 1; $accurate = 0 if(defined($attr{$name}{filterAccuracy}) and defined($dataset->{acc}) and $attr{$name}{filterAccuracy} < $dataset->{acc}); @@ -793,10 +959,12 @@ sub livetracking_Notify($$) Log3 ($name, 4, "$name OwnTracks: ".FmtDateTime($dataset->{tst})." ".$data); - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); if($accurate) { + readingsBulkUpdate($hash, "latitude", $dataset->{lat}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); + readingsBulkUpdate($hash, "longitude", $dataset->{lon}); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "location", $dataset->{lat}.",".$dataset->{lon}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } @@ -804,9 +972,6 @@ sub livetracking_Notify($$) { Log3 ($name, 3, "$name OwnTracks: Inaccurate reading ignored: ".$dataset->{lat}.",".$dataset->{lon}." (".$dataset->{acc}.")"); } - #setReadingsVal($hash, "location", $dataset->{lat}.",".$dataset->{lon}, FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "location: ".$dataset->{lat}.",".$dataset->{lon}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); if(defined($dataset->{alt}) and $dataset->{alt} != 0 and $accurate) @@ -814,84 +979,52 @@ sub livetracking_Notify($$) my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1); my $newaltitude = livetracking_roundfunc($dataset->{alt}/$altitudeRound)*$altitudeRound; #Log3 ($name, 0, "$name OTRound: ".$dataset->{alt}."/".$altitudeRound." = ".livetracking_roundfunc($dataset->{alt}/$altitudeRound)."*".$altitudeRound); - - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "altitude", $newaltitude); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "altitude", $newaltitude." m", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "altitude: ".$newaltitude." m"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } if(defined($dataset->{tid}) and $dataset->{tid} ne "") { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "id", $dataset->{tid}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "id", $dataset->{tid}, FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "id: "$dataset->{tid}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } if(defined($dataset->{doze}) and $dataset->{doze} ne "") { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "doze", $dataset->{doze}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "doze", $dataset->{doze}, FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "doze: "$dataset->{doze}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } if(defined($dataset->{acc}) and $dataset->{acc} > 0)# and $accurate) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "accuracy", $dataset->{acc}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "accuracy", $dataset->{acc}." m", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "accuracy: ".$dataset->{acc}." m"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } if(defined($dataset->{vel}) and $dataset->{vel} >= 0 and $accurate) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "velocity", $dataset->{vel}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "velocity", $dataset->{vel}." km/h", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "velocity: ".$dataset->{vel}." km/h"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } #else #{ # fhem( "deletereading $name velocity" ); + # readingsBulkUpdate($hash, "velocity", 0); + # $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); #} if(defined($dataset->{cog}) and $dataset->{cog} >= 0 and $accurate) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "heading", $dataset->{cog}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "heading", $dataset->{cog}." deg", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "heading: ".$dataset->{cog}." deg"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } #else #{ # fhem( "deletereading $name heading" ); + # readingsBulkUpdate($hash, "heading", 0); + # $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); #} if(defined($dataset->{batt})) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "batteryPercent", $dataset->{batt}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "batteryState", (int($dataset->{batt}) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "battery", $dataset->{batt}." %", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "battery: ".$dataset->{batt}." %"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } if(defined($dataset->{conn})) { @@ -900,7 +1033,7 @@ sub livetracking_Notify($$) } if(defined($dataset->{p}) and $dataset->{p} > 0) { - readingsBulkUpdate($hash, "pressure", $dataset->{p}*10); + readingsBulkUpdate($hash, "pressure", sprintf("%.2f", $dataset->{p}*10)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{desc}) and defined($dataset->{event})) @@ -918,20 +1051,12 @@ sub livetracking_Notify($$) if($dataset->{event} eq "enter") { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "place", $place); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "place", $dataset->{desc}, FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "place: ".$dataset->{desc}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); foreach my $placenumber (@placenumbers) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "zone_".$placenumber,"active"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #readingsSingleUpdate($hash,"zone_".$placenumber,"active",1); } } else @@ -939,11 +1064,8 @@ sub livetracking_Notify($$) #fhem( "deletereading $name place" ) if(ReadingsVal($name,"place","undefined") eq $dataset->{desc}); foreach my $placenumber (@placenumbers) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "zone_".$placenumber,"inactive"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #readingsSingleUpdate($hash,"zone_".$placenumber,"inactive",1); } } } @@ -1002,16 +1124,10 @@ sub livetracking_Notify($$) if(defined($attr{$name}{home}) and $accurate) { - #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); - #setReadingsVal($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km", FmtDateTime($dataset->{tst})); - #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km"); - #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst})); } - #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); readingsEndUpdate($hash, 1); readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1); @@ -1019,7 +1135,52 @@ sub livetracking_Notify($$) $hash->{helper}{lastOwnTracks} = $dataset->{tst}; return undef; +} +sub livetracking_Notify($$) +{ + my ($hash, $dev) = @_; + my $name = $hash->{NAME}; + my $devName = $dev->{NAME}; + + my $dataset = ""; + my $data = ""; + + # Ignore wrong notifications + if($devName eq AttrVal($name, "owntracksDevice" , "owntracks")) + { + Log3 ($name, 6, "$name OwnTracks data: /n".Dumper($dev)); + + my $invaliddata = 1; + if(($dev->{CHANGED}[0] =~ m/_type":[ ]?"location/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"position/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"transition/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"steps/ || $dev->{CHANGED}[0] =~ m/_type":[ ]?"beacon/ )) + { + $invaliddata = 0;#owntracks + Log3 ($name, 4, "$name Detected OwnTracks data from MQTT device notify"); + } + elsif(($dev->{CHANGED}[0] =~ m/position":[ ]?{/)) + { + #{"position":{"id":14935,"attributes":{"batteryLevel":61,"distance":0.06,"totalDistance":4132002.73,"motion":false},"deviceId":1,"type":null,"protocol":"osmand","serverTime":"2018-10-31T22:14:10.290+0000","deviceTime":"2018-10-31T22:14:07.000+0000","fixTime":"2018-10-31T22:14:07.000+0000","outdated":false,"valid":true,"latitude":12.3456789,"longitude":12.3456789,"altitude":0,"speed":0,"course":0,"address":null,"accuracy":19.23500061035156,"network":null},"device":{"id":1,"attributes":{},"groupId":0,"name":"XXX","uniqueId":"YYY","status":"online","lastUpdate":"2018-10-31T22:14:10.290+0000","positionId":14935,"geofenceIds":[2],"phone":"","model":"","contact":"","category":"person","disabled":false}} + #{"position":{"id":566,"attributes":{"batteryLevel":66,"distance":25.79,"totalDistance":20665.79,"motion":false},"deviceId":1,"type":null,"protocol":"osmand","serverTime":"2019-01-06T11:39:41.279+0000","deviceTime":"2019-01-06T11:39:41.000+0000","fixTime":"2019-01-06T11:39:41.000+0000","outdated":false,"valid":true,"latitude":53.xxxxx,"longitude":8.xxxxx,"altitude":0,"speed":0,"course":0,"address":null,"accuracy":24.04000091552734,"network":null},"device":{"id":1,"attributes":{},"groupId":0,"name":"SomeName","uniqueId":"SomeID,"status":"online","lastUpdate":"2019-01-06T11:39:41.279+0000","positionId":565,"geofenceIds":[],"phone":"","model":"","contact":"","category":null,"disabled":false}} + $invaliddata = 0;#traccar + Log3 ($name, 4, "$name Detected Traccar data from MQTT device notify"); + } + if($invaliddata == 1){ + Log3 ($name, 4, "WRONG MQTT TYPE ".Dumper($dev->{CHANGED}[0])); + return undef; + } + + #Log3 ($name, 1, "MQTT ".Dumper($dev->{CHANGED}[0])); + + $data = substr($dev->{CHANGED}[0],index($dev->{CHANGED}[0], ": {")+2); + + } else { + Log3 ($name, 5, "livetracks: Notify ignored from ".$devName); + return undef; + } + + livetracking_ParseOwnTracks($hash,$data); + + return undef; } @@ -1046,14 +1207,22 @@ sub livetracking_dispatch($$$) if( $data !~ /{.*}/ ) { Log3 $name, 3, "$name: invalid json detected: >>$data<< " . $param->{type} if($data ne "[]"); + #$hash->{helper}{life360_token} = "" if( $param->{type} eq 'life360data' ); return undef; } - my $json; - $json = JSON->new->utf8(0)->decode($data); + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dispatch type ".$param->{type}." ".$@; + #Log3 $name, 2, "$name: ".$param->{url}." / ".Dumper($data) if( $param->{type} eq 'life360data' ); + return undef; + } - if( $param->{type} eq 'openpathsdata' ) { + if( $param->{type} eq 'life360data' ) { + livetracking_ParseLife360($hash,$json); + } elsif( $param->{type} eq 'openpathsdata' ) { livetracking_ParseOpenPaths($hash,$json); } elsif( $param->{type} eq 'swarmdata' ) { livetracking_ParseSwarm($hash,$json); @@ -1061,6 +1230,192 @@ sub livetracking_dispatch($$$) } } +############################## + +sub livetracking_BootstrapLife360($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + if(!defined($hash->{helper}{life360_user})) + { + return undef; + } + + if(!defined($hash->{helper}{life360_script}) or $hash->{helper}{life360_script} eq "") + { + my $url = "https://www.life360.com/circles/#/"; + + HttpUtils_NonblockingGet({ + url => $url, + noshutdown => 1, + hash => $hash, + type => 'scriptdata', + callback => \&livetracking_bootstrap, + }); + return undef; + } + + if(!defined($hash->{helper}{life360_secret}) or $hash->{helper}{life360_secret} eq "") + { + my $url = "https://www.life360.com/circles/scripts/".$hash->{helper}{life360_script}.".scripts.js"; + Log3 $name, 1, "$name: $url"; + + HttpUtils_NonblockingGet({ + url => $url, + noshutdown => 1, + hash => $hash, + type => 'secretdata', + callback => \&livetracking_bootstrap, + }); + return undef; + } + + if(!defined($hash->{helper}{life360_token}) or !defined($attr{$name}{life360_userid}) or $hash->{helper}{life360_token} eq "" or $attr{$name}{life360_userid} eq "") + { + my $url = "https://www.life360.com/v3/oauth2/token.json"; + + HttpUtils_NonblockingGet({ + url => $url, + method => "POST", + header => "Content-Type: application/x-www-form-urlencoded\r\nAuthorization: Basic ".$hash->{helper}{life360_secret}, + data => "countryCode=1&password=".uri_escape($hash->{helper}{life360_pass})."&username=".uri_escape($hash->{helper}{life360_user})."&persist=true&grant_type=password", + noshutdown => 1, + hash => $hash, + type => 'tokendata', + callback => \&livetracking_bootstrap, + }); + Log3 $name, 1, "$name: "."countryCode=1&password=".uri_escape($hash->{helper}{life360_pass})."&username=".uri_escape($hash->{helper}{life360_user})."&persist=true&grant_type=password"; + + return undef; + } + + if(!defined($attr{$name}{life360_circle}) or $attr{$name}{life360_circle} eq "") + { + my $url = "https://www.life360.com/v3/circles"; + + HttpUtils_NonblockingGet({ + url => $url, + header => "Authorization: Bearer ".$hash->{helper}{life360_token}, + noshutdown => 1, + hash => $hash, + type => 'circledata', + callback => \&livetracking_bootstrap, + }); + return undef; + } + + + livetracking_GetLife360($hash); + +} + +sub livetracking_bootstrap($$$) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + + if( $err ) { + Log3 $name, 2, "$name: http request failed: $err"; + return undef; + } elsif( $data ) { + Log3 $name, 5, "$name: $data"; + + + if( $param->{type} eq 'scriptdata' ) + { + if ($data =~ /\bscripts\/\b(.*?)\b.scripts.js\b/) + { + $hash->{helper}{life360_script} = $1; + Log3 $name, 4, "$name: life360 script ".$hash->{helper}{life360_script}; + InternalTimer( gettimeofday() + 1, "livetracking_BootstrapLife360", $hash, 0); + } + return undef; + } + elsif( $param->{type} eq 'secretdata' ) + { + if ($data =~ /CLIENT_SECRET = "(.*?)";/) + { + $hash->{helper}{life360_secret} = $1; + Log3 $name, 4, "$name: life360 secret ".$hash->{helper}{life360_secret}; + InternalTimer( gettimeofday() + 1, "livetracking_BootstrapLife360", $hash, 0); + } + return undef; + } + elsif( $param->{type} eq 'tokendata' ) + { + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dispatch type ".$param->{type}." ".$@; + return undef; + } + + $hash->{helper}{life360_token} = $json->{access_token}; + $attr{$name}{life360_userid} = $json->{user}->{id}; + + Log3 $name, 3, "$name: life360 token ".$json->{access_token}; + InternalTimer( gettimeofday() + 1, "livetracking_BootstrapLife360", $hash, 0); + return undef; + } + elsif( $param->{type} eq 'circledata' ) + { + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dispatch type ".$param->{type}." ".$@; + return undef; + } + $attr{$name}{life360_circle} = $json->{circles}[0]->{id} if($json->{circles}[0]->{id} ne ""); + InternalTimer( gettimeofday() + 1, "livetracking_BootstrapLife360", $hash, 0); + + foreach my $dataset (@{$json->{circles}}) + { + Log3 $name, 5, "$name: Life360 Circle: ".$dataset->{name}.", ID: ".$dataset->{id}; + my $url = "https://www.life360.com/v3/circles/".$dataset->{id}; + + HttpUtils_NonblockingGet({ + url => $url, + header => "Authorization: Bearer ".$hash->{helper}{life360_token}, + noshutdown => 1, + hash => $hash, + type => 'familydata', + callback => \&livetracking_bootstrap, + }); + + + } + #Log3 $name, 2, "$name: Life360 : ".Dumper($data); + return undef; + } + elsif( $param->{type} eq 'familydata' ) + { + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if($@) + { + Log3 $name, 2, "$name: invalid json evaluation on dispatch type ".$param->{type}." ".$@; + return undef; + } + InternalTimer( gettimeofday() + 1, "livetracking_GetLife360", $hash, 0); + + foreach my $dataset (@{$json->{members}}) + { + Log3 $name, 2, "$name: Life360 User: ".$dataset->{loginEmail}.", ID: ".$dataset->{id}; + } + #Log3 $name, 5, "$name: Life360 : ".Dumper($data); + + } + + + return undef; + + + } +} + +########################## sub livetracking_getHistory($$$$$) { @@ -1129,7 +1484,7 @@ sub livetracking_addExtension($) { my $url = "/osmand"; delete $data{FWEXT}{$url} if($data{FWEXT}{$url}); - Log3 $name, 1, "Enabling livetracking url for $name"; + Log3 $name, 2, "Enabling livetracking url for $name ".AttrVal($name, "osmandId", ""); $data{FWEXT}{$url}{deviceName} = $name; $data{FWEXT}{$url}{FUNC} = "livetracking_Webcall"; $data{FWEXT}{$url}{LINK} = "livetracking"; @@ -1145,7 +1500,7 @@ sub livetracking_removeExtension($) { my $url = "/osmand"; my $name = $data{FWEXT}{$url}{deviceName}; $name = $hash->{NAME} if(!defined($name)); - Log3 $name, 3, "Disabling livetracking url for $name"; + Log3 $name, 2, "Disabling livetracking url for $name ".AttrVal($name, "osmandId", ""); delete $data{FWEXT}{$url}; delete $modules{"livetracking"}{defptr}{"webcall".AttrVal($name, "osmandId", "")}; } @@ -1154,9 +1509,22 @@ sub livetracking_removeExtension($) { sub livetracking_Webcall() { my ($request) = @_; - my ($id) = $request =~ /id=(.*?)(&|$)/ || ""; + $request =~ /id=(.*?)(&|$)/; + my $id = $1 || ""; + + if($id eq ""){ + $request =~ /"tid":"(.*?)"/; + $id = $1 || ""; + } + Log3 "livetracking", 5, "OsmAnd id incoming: ".$id; + my $hash = $modules{"livetracking"}{defptr}{"webcall".$id}; - $hash = $modules{"livetracking"}{defptr}{"webcall"} if(!defined($hash)); + if(!defined($hash)){ + $hash = $modules{"livetracking"}{defptr}{"webcall"} ; + Log3 "livetracking", 4, "OsmAnd webcall generic" if(defined($hash)); + } else { + Log3 "livetracking", 4, "OsmAnd webcall for specific id"; + } if(!defined($hash)){ Log3 "livetracking", 1, "OsmAnd webcall hash not defined!"; @@ -1166,7 +1534,19 @@ sub livetracking_Webcall() { my $name = $hash->{NAME}; my $osmandid = AttrVal($name, "osmandId", undef); - return undef if(defined($osmandid) && $osmandid ne $id); + if($id ne"" && defined($osmandid) && $osmandid ne $id){ + Log3 "livetracking", 4, "OsmAnd webcall for wrong id"; + return undef; + } + + + if($request =~ /"_type"/){ + $request =~ s/\/osmand&//g; + $request =~ s/\/osmand\/&//g; + Log3 $name, 4, "OwnTracks HTTP request:\n".$request; + livetracking_ParseOwnTracks($hash,$request) if(($request =~ /_type":[ ]?"location/ || $request =~ /_type":[ ]?"position/ || $request =~ /_type":[ ]?"transition/ || $request =~ /_type":[ ]?"steps/ || $request =~ /_type":[ ]?"beacon/ )); + return undef; + } Log3 $name, 5, "OsmAnd webcall request:\n".$request; @@ -1196,6 +1576,10 @@ sub livetracking_Webcall() { if($accurate && defined($lat) && defined($lon)) { + readingsBulkUpdate($hash, "latitude", $lat); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); + readingsBulkUpdate($hash, "longitude", $lon); + $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); readingsBulkUpdate($hash, "location", $lat.",".$lon); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } @@ -1246,8 +1630,8 @@ sub livetracking_Webcall() { if(defined($lat) && defined($lon)) { - return ( "text/plain; charset=utf-8", - "OK" ); + return ( "application/json; charset=UTF-8", + "[]" ); } else { return ( "text/plain; charset=utf-8", "no data" ); @@ -1497,12 +1881,13 @@ sub livetracking_utf8clean($) { =pod =item device -=item summary Position tracking via OwnTracks, OpenPaths and Swarm +=item summary Position tracking via OwnTracks, Life360 and Swarm =begin html

    livetracking

      - This modul provides livetracking data from OpenPaths and Swarm (FourSquare).
      + This modul pulls livetracking data from Life360 and Swarm (FourSquare).
      + Data can also be pushed from OwnTracks or Traccar (iOS).
      Swarm Token: https://foursquare.com/oauth2/authenticate?client_id=EFWJ0DNVIREJ2CY1WDIFQ4MAL0ZGZAZUYCNE0NE0XZC3NCPX&response_type=token&redirect_uri=http://localhost&display=wap

      @@ -1510,8 +1895,8 @@ sub livetracking_utf8clean($) {
        define <name> livetracking <...>
        - Example: define livetrackingdata livetracking [openpaths_key] [openpaths_secret] [swarm_token]
        - Either both, just OpenPaths, just Swarm or none of them can be defined. + Example: define livetrackingdata livetracking [life360_email] [life360_pass] [openpaths_key] [openpaths_secret] [swarm_token]
        + Any combination of these services can be defined as long as their order is correct.
         
      • ...
        @@ -1530,6 +1915,10 @@ sub livetracking_utf8clean($) {
        Manually trigger a data update for OpenPaths

      • +
      • Life360 +
        + Manually trigger a data update for Life360 +

      • Swarm
        Manually trigger a data update for Swarm @@ -1555,6 +1944,10 @@ sub livetracking_utf8clean($) {
        Send a message to OwnTracks

      • +
      • bootstrapLife360 +
        + Re-initialize Life360 login data +


      @@ -1564,6 +1957,14 @@ sub livetracking_utf8clean($) {
      GPS position
      +
    • latitude +
      + GPS position - latitude +

    • +
    • longitude +
      + GPS position - longitude +

    • distance (km)
      GPS distance from home @@ -1681,12 +2082,15 @@ sub livetracking_utf8clean($) {

    • osmandServer (0/1)
      - Starts an OsmAnd compatible listener on FHEM which can be entered into traccar-client directly:
      - https://user:pass@your.fhem.ip/fhem/osmand (The Android client does not support user:pass authentication) + Starts an OsmAnd compatible listener on FHEM which can be entered into traccar-client directly.
      + This is also compatible with OwnTracks HTTP mode.
      + Traccar for Android supports no authentication, OwnTracks may need separate fields instead of user:pass in the address
      + https://user:pass@your.fhem.ip/fhem/osmand (address to be entered in the client)

    • -
    • osmandId +
    • osmandId (if more than one instance is used)
      - The device identifier that is set in the OsmAnd client and transmitted in the request as id + The device identifier that is set in the OsmAnd client and transmitted in the request as id
      + If OwnTracks HTTP mode is used, this can be the TrackerID