############################################## # $Id$$$ 2018-11-01 # # 98_livetracking.pm # # 2019 Markus Moises < vorname at nachname . de > # # This module provides livetracking data from OwnTracks, OpenPaths, Life360 and Swarm (FourSquare) # # ############################################################################## # # define livetracking # ############################################################################## package main; use strict; use warnings; no warnings qw(redefine); #use Math::Round; #use Net::OAuth; use JSON; use Time::Local; use URI::Escape; use Data::Dumper; use Encode qw(encode_utf8 decode_utf8); use utf8; my $libcheck_hasOAuth = 1; ############################################################################## sub livetracking_Initialize($) { my ($hash) = @_; my $name = $hash->{NAME}; eval "use Net::OAuth;"; $libcheck_hasOAuth = 0 if($@); $hash->{DefFn} = "livetracking_Define"; $hash->{UndefFn} = "livetracking_Undefine"; $hash->{GetFn} = "livetracking_Get"; $hash->{SetFn} = "livetracking_Set"; $hash->{AttrFn} = "livetracking_Attr"; $hash->{NotifyFn} = "livetracking_Notify"; $hash->{NotifyOrderPrefix}= "999-"; $hash->{DbLog_splitFn} = "livetracking_DbLog_splitFn"; $hash->{AttrList} = "disable:1 ". "roundAltitude ". "roundDistance ". "filterAccuracy ". "interval ". "home ". "swarmHome ". "owntracksDevice ". "beacon_0 ". "beacon_1 ". "beacon_2 ". "beacon_3 ". "beacon_4 ". "beacon_5 ". "beacon_6 ". "beacon_7 ". "beacon_8 ". "beacon_9 ". "zonename_0 ". "zonename_1 ". "zonename_2 ". "zonename_3 ". "zonename_4 ". "zonename_5 ". "zonename_6 ". "zonename_7 ". "zonename_8 ". "zonename_9 ". "batteryWarning:5,10,15,20,25,30,35,40 ". "addressLanguage:de,en,fr,es,it,nl ". "addressReading:0,1 ". "osmandServer:0,1 ". "osmandId ". "life360_userid ". "life360_circle ". $readingFnAttributes; } sub livetracking_Define($$$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); 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 ) { 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 ) { 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 { require XML::Simple; XML::Simple->import(); 1; }; if($req) { $hash->{NOTIFYDEV} = AttrVal($name, "owntracksDevice" , "owntracks"); } else { $hash->{STATE} = "XML::Simple is required!"; $attr{$name}{disable} = "1"; return undef; } # my $resolve = inet_aton("api.foursquare.com"); # if(!defined($resolve) && defined($hash->{helper}{swarm_token})) # { # $hash->{STATE} = "DNS error"; # InternalTimer( gettimeofday() + 1800, "livetracking_GetAll", $hash, 0); # return undef; # } InternalTimer( gettimeofday() + 60, "livetracking_GetSwarm", $hash, 0) if(defined($hash->{helper}{swarm_token})); # $resolve = inet_aton("openpaths.cc"); # if(!defined($resolve) && defined($hash->{helper}{openpaths_key})) # { # $hash->{STATE} = "DNS error"; # InternalTimer( gettimeofday() + 1800, "livetracking_GetAll", $hash, 0); # return undef; # } InternalTimer( gettimeofday() + 90, "livetracking_GetOpenPaths", $hash, 0) if(defined($hash->{helper}{openpaths_key})); if (!defined($attr{$name}{stateFormat})) { $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; } sub livetracking_Undefine($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); livetracking_removeExtension($hash) if(AttrVal($name, "osmandServer", 0) == 1); return undef; } sub livetracking_Set($$@) { my ($hash, $name, $command, @parameters) = @_; my $usage = "Unknown argument $command, choose one of"; if(defined($attr{$name}{owntracksDevice})) { $usage .= " owntracksMessage"; } if(defined($hash->{helper}{life360_user})) { $usage .= " BootstrapLife360:noArg"; } if(!defined($hash->{helper}{life360_user}) && !defined($attr{$name}{owntracksDevice})) { $usage = undef; } return $usage if $command eq '?'; my $devname=AttrVal($name, "owntracksDevice" , "owntracks" ); if($command eq 'owntracksMessage') { my $messagetext = join( ' ', @parameters ); my $notifytext = ''; $notifytext = '"notify":"FHEM: ' . join( ' ', @parameters ).'",' if($messagetext !~ //); if($messagetext eq "") { $messagetext = ''; } elsif($messagetext !~ //) { $messagetext = '"content":"'.FmtDateTime(time()).'
FHEM:

'.$messagetext.'",'; } 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; } sub livetracking_Get($@) { my ($hash, @a) = @_; my $command = $a[1]; my $parameter = $a[2];# if(defined($a[2])); my $name = $hash->{NAME}; 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 '?'; if(AttrVal($name, "disable", 0) eq 1) { return "livetracking $name is disabled. Aborting..."; } my $devname = AttrVal($name, "owntracksDevice" , "owntracks" ); if($command eq "All") { livetracking_GetAll($hash); } elsif($command eq "OpenPaths") { livetracking_GetOpenPaths($hash); } elsif($command eq "Swarm") { livetracking_GetSwarm($hash); } elsif($command eq 'owntracksLocation') { fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportLocation"}'); return undef; } elsif($command eq 'owntracksSteps') { fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportSteps"}'); return undef; } elsif($command eq 'address') { my @location = split(",",ReadingsVal($name,"location","0,0")); $parameter = "" if(!defined($parameter)); if($parameter =~ /,/){ @location = split(",",$parameter); } my $lang = AttrVal($name,"addressLanguage","en"); if(defined($location[1])) { my($err,$data) = HttpUtils_BlockingGet({ url => "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=".$location[0]."&lon=".$location[1]."&addressdetails=1&accept-language=$lang", noshutdown => 1, }); return "data error" if($err); return "invalid json" if( $data !~ m/^{.*}$/ && $data !~ m/^\[.*\]$/ ); my $json = eval { JSON->new->utf8(0)->decode($data) }; return "invalid json evaluation" if($@); if( $parameter eq "short" && defined($json->{display_name}) ) { readingsSingleUpdate($hash,"address",livetracking_utf8clean($json->{display_name}),1) if(AttrVal($name,"addressReading",0)); return livetracking_utf8clean($json->{display_name}); } elsif( defined($json->{address}) ) { my $addr = ""; 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}->{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 .= (($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 .= (($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); } elsif( defined($json->{display_name}) ) { readingsSingleUpdate($hash,"address",livetracking_utf8clean($json->{display_name}),1) if(AttrVal($name,"addressReading",0)); return livetracking_utf8clean($json->{display_name}); } else { return "no data"; } } else { return "invalid coordinates"; } return undef; } elsif($command eq "Life360") { livetracking_GetLife360($hash); } return undef; } sub livetracking_Attr(@) { my ($command, $name, $attr, $val) = @_; my $hash = $defs{$name}; if ($attr && $attr eq 'owntracksDevice') { $hash->{NOTIFYDEV} = $val if defined $val; } elsif ($attr && $attr =~ /^(zonename_)([0-9]+)/) { fhem( "deletereading $name zone_".$2 ); } elsif ($attr && $attr =~ /^(beacon_)([0-9]+)/) { fhem( "deletereading $name beacon_".$2.".*" ); } elsif ($attr && $attr eq 'osmandServer') { if($command eq "set" && $val == 1){ livetracking_addExtension($hash); } else { livetracking_removeExtension($hash); } } return undef; } sub livetracking_GetAll($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); if(AttrVal($name, "disable", 0) eq 1) { Log3 ($name, 4, "livetracking $name is disabled, data update cancelled."); return undef; } if(defined($attr{$name}{owntracksDevice})) { my $devname=AttrVal($name, "owntracksDevice" , "owntracks" ); fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportLocation"}'); } # my $resolve = inet_aton("api.foursquare.com"); # if(!defined($resolve) && defined($hash->{helper}{swarm_token})) # { # $hash->{STATE} = "DNS error"; # InternalTimer( gettimeofday() + 3600, "livetracking_GetAll", $hash, 0); # return undef; # } InternalTimer( gettimeofday() + 5, "livetracking_GetSwarm", $hash, 0) if(defined($hash->{helper}{swarm_token})); # $resolve = inet_aton("openpaths.cc"); # if(!defined($resolve) && defined($hash->{helper}{openpaths_key})) # { # $hash->{STATE} = "DNS error"; # InternalTimer( gettimeofday() + 3600, "livetracking_GetAll", $hash, 0); # return undef; # } 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; } sub livetracking_GetOpenPaths($) { my ($hash) = @_; my $name = $hash->{NAME}; #RemoveInternalTimer($hash); RemoveInternalTimer($hash, "livetracking_GetOpenPaths"); if(AttrVal($name, "disable", 0) eq 1) { Log3 ($name, 4, "livetracking $name is disabled, data update cancelled."); return undef; } if(!defined($hash->{helper}{openpaths_key})) { return undef; } my $nonce = ""; for (my $i=0;$i<32;$i++) { my $r = int(rand(62)); if ($r<10) { $r += 48; } elsif ($r<36) { $r += 55; } else { $r += 61; } $nonce .= chr($r); } my $request = Net::OAuth->request("request token")->new( consumer_key => $hash->{helper}{openpaths_key}, consumer_secret => $hash->{helper}{openpaths_secret}, request_url => 'https://openpaths.cc/api/1', request_method => 'GET', signature_method => 'HMAC-SHA1', timestamp => livetracking_roundfunc(time()), nonce => $nonce, ); $request->sign; my $lastupdate = livetracking_roundfunc(ReadingsVal($name,".lastOpenPaths",time()-3600)); my $url = $request->to_url."&start_time=".$lastupdate."&num_points=50"; # start_time/end_time/num_points Log3 ($name, 4, "livetracking OpenPaths URL: ".$url); HttpUtils_NonblockingGet({ url => $url, timeout => 10, noshutdown => 1, hash => $hash, type => 'openpathsdata', callback => \&livetracking_dispatch, }); my $interval = AttrVal($hash->{NAME}, "interval", 1800); #RemoveInternalTimer($hash); InternalTimer( gettimeofday() + $interval, "livetracking_GetOpenPaths", $hash, 0); $hash->{UPDATED} = FmtDateTime(time()); return undef; } sub livetracking_GetSwarm($) { my ($hash) = @_; my $name = $hash->{NAME}; #RemoveInternalTimer($hash); RemoveInternalTimer($hash, "livetracking_GetSwarm"); if(AttrVal($name, "disable", 0) eq 1) { Log3 ($name, 4, "livetracking $name is disabled, data update cancelled."); return undef; } if(!defined($hash->{helper}{swarm_token})) { return undef; } my $lastupdate = livetracking_roundfunc(ReadingsVal($name,".lastSwarm",time()-3600)); my $url = "https://api.foursquare.com/v2/users/self/checkins?oauth_token=".$hash->{helper}{swarm_token}."&v=20150516&sort=oldestfirst&limit=25&afterTimestamp=".$lastupdate; HttpUtils_NonblockingGet({ url => $url, timeout => 10, noshutdown => 1, hash => $hash, type => 'swarmdata', callback => \&livetracking_dispatch, }); my $interval = AttrVal($hash->{NAME}, "interval", 900); #RemoveInternalTimer($hash); InternalTimer( gettimeofday() + $interval, "livetracking_GetSwarm", $hash, 0); $hash->{UPDATED} = FmtDateTime(time()); return undef; } sub livetracking_ParseLife360($$) { my ($hash,$json) = @_; my $name = $hash->{NAME}; my $updated = 0; my $lastreading = ReadingsVal($name,".lastLife360",time()-24*60*60); 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, 6, "$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, 4, "$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", sprintf("%.5f", $dataset->{latitude})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); readingsBulkUpdate($hash, "longitude", sprintf("%.5f", $dataset->{longitude})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); readingsBulkUpdate($hash, "location", sprintf("%.5f", $dataset->{latitude}).",".sprintf("%.5f", $dataset->{longitude})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); } if(defined($dataset->{speed}) and $dataset->{speed} >= 0 and $accurate) { readingsBulkUpdate($hash, "velocity", int($dataset->{speed})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{endTimestamp}); } readingsBulkUpdate($hash, "accuracy", int($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", int($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", int($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}; my $updated = 0; my $lastreading = ReadingsVal($name,".lastOpenPaths",time()-300); my $device = ReadingsVal($name,"deviceOpenPaths",""); my $os = ReadingsVal($name,"osOpenPaths",""); my $version = ReadingsVal($name,"versionOpenPaths",""); my $altitude = ReadingsVal($name,"altitude","0"); my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1); Log3 ($name, 6, "$name OpenPaths data: /n".Dumper($json)); foreach my $dataset (@{$json}) { Log3 ($name, 5, "$name OpenPaths: at ".FmtDateTime($dataset->{t})." / ".$dataset->{lat}.",".$dataset->{lon}); $lastreading = $dataset->{t}+1; readingsBeginUpdate($hash); # Begin update readings $hash->{".updateTimestamp"} = FmtDateTime($dataset->{t}); my $changeindex = 0; readingsBulkUpdate($hash, "latitude", sprintf("%.5f", $dataset->{lat})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); readingsBulkUpdate($hash, "longitude", sprintf("%.5f", $dataset->{lon})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); readingsBulkUpdate($hash, "location", sprintf("%.5f", $dataset->{lat}).",".sprintf("%.5f", $dataset->{lon})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); if(defined($dataset->{alt}) && $dataset->{alt} ne '') { my $newaltitude = livetracking_roundfunc($dataset->{alt}/$altitudeRound)*$altitudeRound; #Log3 ($name, 0, "$name SwarmRound: ".$dataset->{alt}."/".$altitudeRound." = ".livetracking_roundfunc($dataset->{alt}/$altitudeRound)." *".$altitudeRound); if($altitude ne $newaltitude) { readingsBulkUpdate($hash, "altitude", int($newaltitude)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); $altitude = $newaltitude; } } if(defined($dataset->{device}) && $dataset->{device} ne $device) { readingsBulkUpdate($hash, "deviceOpenPaths", $dataset->{device}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); } if(defined($dataset->{os}) && $dataset->{os} ne $os) { readingsBulkUpdate($hash, "osOpenPaths", $dataset->{os}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); } if(defined($dataset->{version}) && $dataset->{version} ne $version) { readingsBulkUpdate($hash, "versionOpenPaths", $dataset->{version}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); } if(defined($attr{$name}{home})) { readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t}); } $updated = 1; readingsEndUpdate($hash, 1); # End update readings } if($updated == 1) { readingsSingleUpdate($hash,".lastOpenPaths",$lastreading,1); $hash->{helper}{lastOpenPaths} = $lastreading; } return undef; } sub livetracking_ParseSwarm($$) { my ($hash,$json) = @_; my $name = $hash->{NAME}; my $updated = 0; my $lastreading = ReadingsVal($name,".lastSwarm",time()-300); my $device = ReadingsVal($name,"deviceSwarm",""); Log3 ($name, 6, "$name Swarm data: /n".Dumper($json)); foreach my $dataset (@{$json->{response}->{checkins}->{items}}) { next if(!defined($dataset->{type}) || $dataset->{type} ne "checkin"); readingsBeginUpdate($hash); $hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt}); my $changeindex = 0; $lastreading = $dataset->{createdAt}+1; my $place = livetracking_utf8clean($dataset->{venue}->{name}); Log3 ($name, 4, "$name Swarm: ".$place." at ".FmtDateTime($dataset->{createdAt})." / ".$dataset->{venue}->{location}->{lat}.",".$dataset->{venue}->{location}->{lng}); my $loc = sprintf("%.5f", $dataset->{venue}->{location}->{lat}).",".sprintf("%.5f", $dataset->{venue}->{location}->{lng}); if(defined($attr{$name}{swarmHome}) and defined($attr{$name}{home})) { my $shl = $attr{$name}{swarmHome}; my $home = $attr{$name}{home}; $loc =~ s/$shl/$home/g; } readingsBulkUpdate($hash, "latitude", sprintf("%.5f", $dataset->{venue}->{location}->{lat})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); readingsBulkUpdate($hash, "longitude", sprintf("%.5f", $dataset->{venue}->{location}->{lng})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); readingsBulkUpdate($hash, "location", $loc); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); readingsBulkUpdate($hash, "place", $place); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); if(defined($dataset->{source}->{name}) && $dataset->{source}->{name} ne $device) { readingsBulkUpdate($hash, "deviceSwarm", $dataset->{source}->{name}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); } if(defined($attr{$name}{home})) { readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$loc,$attr{$name}{home})." km"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); } $updated = 1; #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt}); readingsEndUpdate($hash, 1); } if($updated == 1) { #readingsSingleUpdate($hash,"lastSwarm",$lastreading,1); #$hash->{CHANGED} = (); #$hash->{CHANGETIME} = (); readingsSingleUpdate($hash,".lastSwarm",$lastreading,1); $hash->{helper}{lastSwarm} = $lastreading; } return undef; } sub livetracking_ParseOwnTracks { my ($hash,$data) = @_; my $name = $hash->{NAME}; #//rcmcronny CSV DECODING if($data =~ m/[A-Za-z0-9]{2},[A-Za-z0-9]{8},[f|c|a|k|L|l|m|t|T|v|s]{1},[0-9]{8},[0-9]{8},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5}$/) { Log3 ($name, 4, "$name - CSV encoded payload detected -> ".Dumper($data)); my ($csv_tid, $csv_tst, $csv_t, $csv_lat, $csv_lon, $csv_cog, $csv_vel, $csv_alt, $csv_dist, $csv_trip) = $data =~ m/([A-Za-z0-9]{2}),([A-Za-z0-9]{8}),([f|c|a|k|L|l|m|t|T|v|s]{1}),([0-9]{8}),([0-9]{8}),([0-9]{1,5}),([0-9]{1,5}),([0-9]{1,5}),([0-9]{1,5}),([0-9]{1,5})$/; $data = '{ "_type": "location", "t": "'.$csv_t.'", "tst": "'.(hex($csv_tst)).'", "tid": "'.$csv_tid.'", "lat": "'.($csv_lat/1000000).'", "lon": "'.($csv_lon/1000000).'", "alt": "'.($csv_alt*10).'", "vel": "'.$csv_vel.'", "cog": "'.($csv_cog*10).'", "dist": "'.$csv_dist.'", "trip": "'.($csv_trip*1000).'" }'; } my $dataset = eval { JSON->new->utf8(0)->decode($data) }; if($@) { Log3 $name, 2, "$name: invalid json evaluation on ParseOwnTracks".Dumper($data); return undef; } if($data =~ m/_type":[ ]?"steps/) { readingsBeginUpdate($hash); # Start update readings $hash->{".updateTimestamp"} = FmtDateTime($dataset->{to}); readingsBulkUpdate($hash, "steps", int($dataset->{steps})); $hash->{CHANGETIME}[0] = FmtDateTime($dataset->{to}); readingsBulkUpdate($hash, "walking", int($dataset->{distance})); $hash->{CHANGETIME}[1] = FmtDateTime($dataset->{to}); readingsBulkUpdate($hash, "floorsup", int($dataset->{floorsup})); $hash->{CHANGETIME}[2] = FmtDateTime($dataset->{to}); readingsBulkUpdate($hash, "floorsdown", int($dataset->{floorsdown})); $hash->{CHANGETIME}[3] = FmtDateTime($dataset->{to}); readingsEndUpdate($hash, 1); readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1); $hash->{helper}{lastOwnTracks} = $dataset->{tst}; return undef; } if($data =~ m/_type":[ ]?"beacon/) { my $beaconid = $dataset->{uuid}.",".$dataset->{major}.",".$dataset->{minor}; readingsBeginUpdate($hash); # Start update readings $hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "beacon", $beaconid); $hash->{CHANGETIME}[0] = FmtDateTime($dataset->{tst}); for(my $i=9;$i>=0;$i--) { next if(!defined($attr{$name}{"beacon_$i"})); if($beaconid eq $attr{$name}{"beacon_$i"}) { readingsBulkUpdate($hash, "beacon_".$i."_proximity", int($dataset->{prox})); $hash->{CHANGETIME}[1] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "beacon_".$i."_accuracy", sprintf("%.2f", $dataset->{acc})); $hash->{CHANGETIME}[2] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "beacon_".$i."_rssi", $dataset->{rssi}); $hash->{CHANGETIME}[3] = FmtDateTime($dataset->{tst}); last; } } readingsEndUpdate($hash, 1); readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1); $hash->{helper}{lastOwnTracks} = $dataset->{tst}; return undef; } #{"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}); readingsBeginUpdate($hash); # Start update readings $hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst}); my $changeindex = 0; Log3 ($name, 4, "$name OwnTracks: ".FmtDateTime($dataset->{tst})." ".$data); if($accurate) { readingsBulkUpdate($hash, "latitude", sprintf("%.5f", $dataset->{lat})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "longitude", sprintf("%.5f", $dataset->{lon})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); readingsBulkUpdate($hash, "location", sprintf("%.5f", $dataset->{lat}).",".sprintf("%.5f", $dataset->{lon})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } else { Log3 ($name, 3, "$name OwnTracks: Inaccurate reading ignored: ".$dataset->{lat}.",".$dataset->{lon}." (".$dataset->{acc}.")"); } if(defined($dataset->{alt}) and $dataset->{alt} != 0 and $accurate) { 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); readingsBulkUpdate($hash, "altitude", int($newaltitude)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{tid}) and $dataset->{tid} ne "") { readingsBulkUpdate($hash, "id", $dataset->{tid}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{doze}) and $dataset->{doze} ne "") { readingsBulkUpdate($hash, "doze", $dataset->{doze}); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{acc}) and $dataset->{acc} > 0)# and $accurate) { readingsBulkUpdate($hash, "accuracy", int($dataset->{acc})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{vel}) and $dataset->{vel} >= 0 and $accurate) { readingsBulkUpdate($hash, "velocity", int($dataset->{vel})); $hash->{CHANGETIME}[$changeindex++] = 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) { readingsBulkUpdate($hash, "heading", int($dataset->{cog})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } #else #{ # fhem( "deletereading $name heading" ); # readingsBulkUpdate($hash, "heading", 0); # $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); #} if(defined($dataset->{batt})) { readingsBulkUpdate($hash, "batteryPercent", int($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}); } if(defined($dataset->{conn})) { readingsBulkUpdate($hash, "connection", (($dataset->{conn} eq "m")?"mobile":($dataset->{conn} eq "w")?"wifi":($dataset->{conn} eq "o")?"offline":"unknown")); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{p}) and $dataset->{p} > 0) { readingsBulkUpdate($hash, "pressure", sprintf("%.2f", $dataset->{p}*10)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{desc}) and defined($dataset->{event})) { Log3 ($name, 4, "$name OwnTracks Zone Event: ".$dataset->{event}." ".$dataset->{desc}); my $place = livetracking_utf8clean($dataset->{desc}); my @placenumbers; for(my $i=9;$i>=0;$i--) { next if(!defined($attr{$name}{"zonename_$i"})); push @placenumbers, $i if($place =~ m/^($attr{$name}{"zonename_$i"})$/); } if($dataset->{event} eq "enter") { readingsBulkUpdate($hash, "place", $place); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); foreach my $placenumber (@placenumbers) { readingsBulkUpdate($hash, "zone_".$placenumber,"active"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } } else { #fhem( "deletereading $name place" ) if(ReadingsVal($name,"place","undefined") eq $dataset->{desc}); foreach my $placenumber (@placenumbers) { readingsBulkUpdate($hash, "zone_".$placenumber,"inactive"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } } } if(defined($dataset->{t})) { my $trigger = "unknown"; $trigger = "ping" if($dataset->{t} eq "p"); $trigger = "region" if($dataset->{t} eq "c"); $trigger = "beacon" if($dataset->{t} eq "b"); $trigger = "request" if($dataset->{t} eq "r"); $trigger = "manual" if($dataset->{t} eq "u"); $trigger = "timer" if($dataset->{t} eq "t"); $trigger = "frequent" if($dataset->{t} eq "v"); readingsBulkUpdate($hash, "trigger",$trigger); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } else { readingsBulkUpdate($hash, "trigger","automatic"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } if(defined($dataset->{inregions})) { my @placenumbersactive; my @placenumbersinactive; for(my $i=9;$i>=0;$i--) { next if(!defined($attr{$name}{"zonename_$i"})); my $active = 0; foreach my $regionname ( @{$dataset->{inregions}} ) { $active = 1 if($regionname =~ m/^($attr{$name}{"zonename_$i"})$/); } if($active){ Log3 ($name, 4, "$name OwnTracks region active: ".Dumper($attr{$name}{"zonename_$i"})); push @placenumbersactive, $i; } else { Log3 ($name, 4, "$name OwnTracks region inactive: ".Dumper($attr{$name}{"zonename_$i"})); push @placenumbersinactive, $i; } } foreach my $placenumber (@placenumbersactive) { readingsBulkUpdate($hash, "zone_".$placenumber,"active"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } foreach my $placenumber (@placenumbersinactive) { readingsBulkUpdate($hash, "zone_".$placenumber,"inactive"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } } if(defined($attr{$name}{home}) and $accurate) { readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst}); } readingsEndUpdate($hash, 1); if(defined($dataset->{desc}) and defined($dataset->{event})) #DoTrigger after readingsEndUpdate! { DoTrigger($name, $dataset->{event}.": ".$dataset->{desc}); Log3 ($name, 4, "$name OwnTracks Zone Event Trigger: ".$dataset->{event}." ".$dataset->{desc}); } readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1); $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"); } elsif(($dev->{CHANGED}[0] =~ m/[A-Za-z0-9]{2},[A-Za-z0-9]{8},[f|c|a|k|L|l|m|t|T|v|s]{1},[0-9]{8},[0-9]{8},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5},[0-9]{1,5}$/)) { $invaliddata = 0;#owntracks-csv Log3 ($name, 4, "$name Detected OwnTracks CSV ENCODED 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; } ########################## sub livetracking_dispatch($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if( $err ) { Log3 $name, 2, "$name: http request failed: $err"; } elsif( $data ) { Log3 $name, 5, "$name: $data"; $data =~ s/\n//g; 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 = 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 'life360data' ) { livetracking_ParseLife360($hash,$json); } elsif( $param->{type} eq 'openpathsdata' ) { livetracking_ParseOpenPaths($hash,$json); } elsif( $param->{type} eq 'swarmdata' ) { livetracking_ParseSwarm($hash,$json); } } } ############################## 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"; 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, }); 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($$$$$) { my ($param,$f,$t,$srcDesc,$showData) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my (@da, $ret, @vals); my @keys = ("min","mindate","max","maxdate","currval","currdate", "firstval","firstdate","avg","cnt","lastraw"); foreach my $src (@{$srcDesc->{order}}) { my $s = $srcDesc->{src}{$src}; my $fname = ($src eq $defs{$name}{LOGDEVICE} ? $defs{$name}{LOGFILE} : "CURRENT"); my $cmd = "get $src $fname INT $f $t ".$s->{arg}; FW_fC($cmd, 1); if($showData) { $ret .= "\n$cmd\n\n"; $ret .= $$internal_data if(ref $internal_data eq "SCALAR"); } else { push(@da, $internal_data); for(my $i = 0; $i<=$s->{idx}; $i++) { my %h; foreach my $k (@keys) { $h{$k} = $data{$k.($i+1)}; } push @vals, \%h; } } } # Reorder the $data{maxX} stuff my ($min, $max) = (999999, -999999); my $no = int(keys %{$srcDesc->{rev}}); for(my $oi = 0; $oi < $no; $oi++) { my $nl = int(keys %{$srcDesc->{rev}{$oi}}); for(my $li = 0; $li < $nl; $li++) { my $r = $srcDesc->{rev}{$oi}{$li}+1; my $val = shift @vals; foreach my $k (@keys) { $min = $val->{$k} if($k eq "min" && defined($val->{$k}) && $val->{$k} =~ m/[-+]?\d*\.?\d+/ && $val->{$k} < $min); $max = $val->{$k} if($k eq "max" && defined($val->{$k}) && $val->{$k} =~ m/[-+]?\d*\.?\d+/ && $val->{$k} > $max); $data{"$k$r"} = $val->{$k}; } } } $data{maxAll} = $max; $data{minAll} = $min; return $ret if($showData); return \@da; } ######################### sub livetracking_addExtension($) { my ($hash) = @_; my $name = $hash->{NAME}; #livetracking_removeExtension() ; my $url = "/osmand"; delete $data{FWEXT}{$url} if($data{FWEXT}{$url}); 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"; $modules{"livetracking"}{defptr}{"webcall".AttrVal($name, "osmandId", "")} = $hash; } ######################### sub livetracking_removeExtension($) { my ($hash) = @_; my $url = "/osmand"; my $name = $data{FWEXT}{$url}{deviceName}; $name = $hash->{NAME} if(!defined($name)); Log3 $name, 2, "Disabling livetracking url for $name ".AttrVal($name, "osmandId", ""); delete $data{FWEXT}{$url}; delete $modules{"livetracking"}{defptr}{"webcall".AttrVal($name, "osmandId", "")}; } ######################### sub livetracking_Webcall() { my ($request) = @_; $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}; 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!"; return ( "text/plain; charset=utf-8", "undefined" ); } my $name = $hash->{NAME}; my $osmandid = AttrVal($name, "osmandId", undef); 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 ( "text/plain; charset=utf-8", "[]" ); } Log3 $name, 5, "OsmAnd webcall request:\n".$request; my ($tst) = $request =~ /tamp=(.*?)(&|$)/; my ($hdop) = $request =~ /hdop=(.*?)(&|$)/ || 0; my ($lat) = $request =~ /lat=(.*?)(&|$)/; my ($lon) = $request =~ /lon=(.*?)(&|$)/; my ($speed) = $request =~ /speed=(.*?)(&|$)/; my ($bearing) = $request =~ /bearing=(.*?)(&|$)/; my ($altitude) = $request =~ /altitude=(.*?)(&|$)/; my ($battery) = $request =~ /batt=(.*?)(&|$)/; if(!defined($tst)) { return ( "text/plain; charset=utf-8", "timestamp missing" ); } my $accurate = 1; $accurate = 0 if(defined($attr{$name}{filterAccuracy}) and defined($hdop) and $attr{$name}{filterAccuracy} < $hdop); my $changeindex = 0; readingsBeginUpdate($hash); # Start update readings $hash->{".updateTimestamp"} = FmtDateTime($tst); Log3 ($name, 4, "$name OsmAnd Server: ".FmtDateTime($tst)); if($accurate && defined($lat) && defined($lon)) { readingsBulkUpdate($hash, "latitude", sprintf("%.5f", $lat)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); readingsBulkUpdate($hash, "longitude", sprintf("%.5f", $lon)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); readingsBulkUpdate($hash, "location", sprintf("%.5f", $lat).",".sprintf("%.5f", $lon)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } else { Log3 ($name, 3, "$name OsmAnd: Inaccurate reading ignored: ".$lat.",".$lon." (".$hdop.")"); } if($accurate && defined($speed) && $speed >= 0) { readingsBulkUpdate($hash, "velocity", int($speed)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } if($accurate && defined($bearing) && $bearing >= 0) { readingsBulkUpdate($hash, "heading", int($bearing)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } if($accurate && defined($altitude) and $altitude != 0) { my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1); my $newaltitude = livetracking_roundfunc($altitude/$altitudeRound)*$altitudeRound; readingsBulkUpdate($hash, "altitude", int($newaltitude)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } if(defined($hdop) && $hdop > 0) { readingsBulkUpdate($hash, "accuracy", int($hdop)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } if(defined($battery)) { readingsBulkUpdate($hash, "batteryPercent", int($battery)); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); readingsBulkUpdate($hash, "batteryState", (int($battery) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok"); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } if($accurate && defined($attr{$name}{home}) and defined($lat) && defined($lon)) { readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$lat.",".$lon,$attr{$name}{home})); $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst); } readingsEndUpdate($hash, 1); readingsSingleUpdate($hash,".lastOsmAnd",$tst,1); $hash->{helper}{lastOsmAnd} = $tst; if(defined($lat) && defined($lon)) { return ( "application/json; charset=UTF-8", "[]" ); } else { return ( "text/plain; charset=utf-8", "no data" ); } return ( "text/plain; charset=utf-8", "undefined" ); } ########################## sub livetracking_DbLog_splitFn($) { my ($event) = @_; my ($reading, $value, $unit) = ""; Log3 ("dbsplit", 5, "event ".$event); my @parts = split(/ /,$event,3); $reading = $parts[0]; $reading =~ tr/://d; $value = $parts[1]; #Log3 ("dbsplit", 5, "split ".$parts[0]." / ".$parts[1]." / ".$parts[2]); Log3 ("dbsplit", 5, "split ".$event); if($event =~ m/altitude/) { $reading = 'altitude'; $unit = 'm'; } elsif($event =~ m/accuracy/) { $reading = 'accuracy'; $unit = 'm'; } elsif($event =~ m/distance/) { $reading = 'distance'; $unit = 'km'; } elsif($event =~ m/velocity/) { $reading = 'velocity'; $unit = 'km/h'; } elsif($event =~ m/heading/) { $reading = 'heading'; $unit = 'deg'; } elsif($event =~ m/batteryPercent/) { $reading = 'batteryPercent'; $unit = '%'; } elsif($event =~ m/batteryState/) { $reading = 'batteryState'; $unit = ''; } elsif($event =~ m/steps/) { $reading = 'steps'; $unit = 'steps'; } elsif($event =~ m/walking/) { $reading = 'walking'; $unit = 'm'; } elsif($event =~ m/floorsup/) { $reading = 'floorsup'; $unit = 'floors'; } elsif($event =~ m/floorsdown/) { $reading = 'floorsdown'; $unit = 'floors'; } elsif($event =~ m/pressure/) { $reading = 'pressure'; $unit = 'mbar'; } else { $value = $parts[1]; $value = $value." ".$parts[2] if(defined($parts[2])); } #Log3 ("dbsplit", 5, "output ".$reading." / ".$value." / ".$unit); return ($reading, $value, $unit); } ########################## sub livetracking_distance($$$) { my ($hash, $loc1, $loc2) = @_; my $name = $hash->{NAME}; my @location1 = split(',', $loc1); my @location2 = split(',', $loc2); my $lat1 = $location1[0]; my $lon1 = $location1[1]; my $lat2 = $location2[0]; my $lon2 = $location2[1]; my $theta = $lon1 - $lon2; my $dist = sin(livetracking_deg2rad($lat1)) * sin(livetracking_deg2rad($lat2)) + cos(livetracking_deg2rad($lat1)) * cos(livetracking_deg2rad($lat2)) * cos(livetracking_deg2rad($theta)); $dist = livetracking_acos($dist); $dist = livetracking_rad2deg($dist); my $round = AttrVal($hash->{NAME}, "roundDistance", 0.1); $dist = $dist * 60 / $round * 1.85316; #Log3 ($name, 0, "$name DistRound: ".$dist."=".livetracking_roundfunc($dist)."*".$round); return livetracking_roundfunc($dist)*$round; } sub livetracking_roundfunc($) { my ($number) = @_; return sprintf("%.0f", $number); #return Math::Round::round($number); } sub livetracking_acos($) { my ($rad) = @_; my $ret = atan2(sqrt(1 - $rad**2), $rad); return $ret; } sub livetracking_deg2rad($) { my ($deg) = @_; my $pi = atan2(1,1) * 4; return ($deg * $pi / 180); } sub livetracking_rad2deg($) { my ($rad) = @_; my $pi = atan2(1,1) * 4; return ($rad * 180 / $pi); } ########################## sub livetracking_utf8clean($) { my ($string) = @_; my $log = ""; return $string if(utf8::is_utf8($string)); return encode_utf8($string); if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/) { $log .= $string."(standard) "; $string =~ s/Ä/Ae/g; $string =~ s/Ö/Oe/g; $string =~ s/Ü/Ue/g; $string =~ s/ä/ae/g; $string =~ s/ö/oe/g; $string =~ s/ü/ue/g; $string =~ s/ß/ss/g; } if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/) { $log .= $string."(single) "; $string =~ s/Ä/Ae/g; $string =~ s/Ö/Oe/g; $string =~ s/Ü/Ue/g; $string =~ s/ä/ae/g; $string =~ s/ö/oe/g; $string =~ s/ü/ue/g; $string =~ s/ß/ss/g; } if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/) { $log .= $string."(double) "; $string =~ s/Ä/Ae/g; $string =~ s/Ö/Oe/g; $string =~ s/Ü/Ue/g; $string =~ s/ä/ae/g; $string =~ s/ö/oe/g; $string =~ s/ü/ue/g; $string =~ s/ß/ss/g; } if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/) { $log .= $string."(unknown)"; $string =~ s/[ÀÁÂÃĀĂȦẢÅǍȀȂĄẠḀẦẤẪẨẰẮẴẲǠǞǺẬẶȺⱭⱯⱰÆǼǢ]/A/g; $string =~ s/[ḂƁḄḆƂƄɃℬ]/B/g; $string =~ s/[ĆĈĊČƇÇḈȻ©℃]/C/g; $string =~ s/[ḊƊḌḎḐḒĎÐĐƉƋ]/D/g; $string =~ s/[ÈÉÊẼĒĔĖËẺĚȄȆẸȨĘḘḚỀẾỄỂḔḖỆḜƎɆƐƏ]/E/g; $string =~ s/[ḞƑ℉]/F/g; $string =~ s/[ǴĜḠĞĠǦƓĢǤ]/G/g; $string =~ s/[ĤḦȞḤḨḪĦⱧⱵǶℌ]/H/g; $string =~ s/[ÌÍÎĨĪĬİÏỈǏỊĮȈȊḬƗḮℑ]/I/g; $string =~ s/[IJĴɈ]/J/g; $string =~ s/[ḰǨḴƘḲĶⱩ]/K/g; $string =~ s/[ĹḺḶĻḼĽĿŁḸȽⱠⱢ]/L/g; $string =~ s/[ḾṀṂⱮƜℳ]/M/g; $string =~ s/[ǸŃÑṄŇŊƝṆŅṊṈȠ№]/N/g; $string =~ s/[ÒÓÔÕŌŎȮỎŐǑȌȎƠǪỌƟØỒỐỖỔȰȪȬṌṐṒỜỚỠỞỢǬǾƆŒƢ]/O/g; $string =~ s/[ṔṖƤⱣ℗]/P/g; $string =~ s/[Ɋ]/Q/g; $string =~ s/[ŔṘŘȐȒṚŖṞṜƦɌⱤ®Ω]/R/g; $string =~ s/[ŚŜṠŠṢȘŞⱾṤṦṨƧ℠]/S/g; $string =~ s/[ṪŤƬƮṬȚŢṰṮŦȾ™]/T/g; $string =~ s/[ÙÚÛŨŪŬỦŮŰǓȔȖƯỤṲŲṶṴṸṺǛǗǕǙỪỨỮỬỰɄ]/U/g; $string =~ s/[ṼṾƲɅ]/V/g; $string =~ s/[ẀẂŴẆẄẈⱲ]/W/g; $string =~ s/[ẊẌ]/X/g; $string =~ s/[ỲÝŶỸȲẎŸỶƳỴɎ]/Y/g; $string =~ s/[ŹẐŻŽȤẒẔƵⱿⱫℨ]/Z/g; $string =~ s/[àáâãāăȧäảåǎȁȃąạḁẚầấẫẩằắẵẳǡǟǻậặⱥɑɐɒæǽǣª]/a/g; $string =~ s/[ḃɓḅḇƀƃƅ]/b/g; $string =~ s/[ćĉċčƈçḉȼ]/c/g; $string =~ s/[ḋɗḍḏḑḓďđƌȡÞþ]/d/g; $string =~ s/[èéêẽēĕėëẻěȅȇẹȩęḙḛềếễểḕḗệḝɇɛǝⱸⱻ]/e/g; $string =~ s/[ḟƒ]/f/g; $string =~ s/[ǵĝḡğġǧɠģǥℊ]/g/g; $string =~ s/[ĥḣḧȟḥḩḫẖħⱨⱶƕ]/h/g; $string =~ s/[ìíîĩīĭıïỉǐịįȉȋḭɨḯℹ︎]/i/g; $string =~ s/[ijĵǰȷɉ]/j/g; $string =~ s/[ḱǩḵƙḳķĸⱪ]/k/g; $string =~ s/[ĺḻḷļḽľŀłƚḹȴⱡ]/l/g; $string =~ s/[ḿṁṃɱɯ]/m/g; $string =~ s/[ǹńñṅňŋɲṇņṋṉʼnƞȵ]/n/g; $string =~ s/[òóôõōŏȯöỏőǒȍȏơǫọɵøồốỗổȱȫȭṍṏṑṓờớỡởợǭộǿɔœƍⱷⱺƣº]/o/g; $string =~ s/[ṕṗƥ]/p/g; $string =~ s/[ɋ]/q/g; $string =~ s/[ŕṙřȑȓṛŗṟṝɍⱹ]/r/g; $string =~ s/[śŝṡšȿṥṧṩƨßſẛ]/s/g; $string =~ s/[ṫẗťƭʈƫṭțţṱṯŧⱦȶ]/t/g; $string =~ s/[ùúûũūŭüủůűǔȕưụṳųṷṵṹṻǜǘǖǚừứữửựʉµ]/u/g; $string =~ s/[ṽṿⱱⱴʌ]/v/g; $string =~ s/[ẁẃŵẇẅẘẉⱳ]/w/g; $string =~ s/[ẋẍ]/x/g; $string =~ s/[ỳýŷȳẏÿỷẙƴỵɏ]/y/g; $string =~ s/[źẑżžȥẓẕƶɀⱬ]/z/g; #$string =~ s/[^!-~\s]//g; $string =~ s/[^\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]//g; } Log3 "utf8clean", 4, "Cleaned $string // $log" if($log ne ""); return $string;} 1; =pod =item device =item summary Position tracking via OwnTracks, Life360 and Swarm =begin html

livetracking

    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

    Define
      define <name> livetracking <...>
      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.
       
    • ...
      Reverse geocoding: Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright


    Get
    Set
    Readings
    • location
      GPS position

    • latitude
      GPS position - latitude

    • longitude
      GPS position - longitude

    • distance (km)
      GPS distance from home

    • accuracy (m)
      GPS accuracy

    • altitude (m)
      GPS altitude

    • velocity (km/h)
      GPS velocity

    • heading (deg)
      GPS heading

    • place
      Swarm place name

    • steps (steps)
      iOS walked steps

    • walking (m)
      iOS walked distance

    • floorsup (floors)
      iOS floors walked up

    • floorsdown (floors)
      iOS floors walked down

    • zone_N (active/inactive)
      Zone status in OwnTracks

    • beacon
      Beacon ID from OwnTracks

    • beacon_N_X
      Beacon data for saved beacons for indoor positioning

    • batteryState (ok/low)
      Battery state (can be set through attribute batteryWarning )

    • batteryPercent (%)
      Battery percentage

    • connection (mobile/wifi/offline/unknown)
      Phone connection type from OwnTracks at last position


    Attributes
    • batteryWarning (%)
      Set battery ok/low threshold

    • beacon_N
      Saved beacon IDs from OwnTracks for indoor positioning, e.g.:
      FDA50693-A4E2-4FB1-AFCF-C6EB07647825,19789,1

    • zonename_N
      Assign zone name from OwnTracks

    • home (lat,lon)
      Home location

    • swarmHome (lat,lon)
      Fake home location (that is assigned to private homes for security reasons) of your Swarm home (exact position)

    • filterAccuracy (m)
      Minimum accuracy of GPS location to update any readings

    • roundDistance (km)
      Rounding for distance reading to prevent too many changes

    • roundAltitude (m)
      Rounding for altitude reading to prevent too many changes

    • owntracksDevice
      OwnTracks MQTT device to look for notifies from

    • addressLanguage (de/en/fr/es/it/nl)
      Preferred language used to return reverse geocoding results

    • addressReading (0/1)
      Write reverse geocoding results to address reading

    • osmandServer (0/1)
      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 (if more than one instance is used)
      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

=end html =cut