diff --git a/fhem/CHANGED b/fhem/CHANGED
index 6e797ae9c..00b2402ef 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.
+  - new:     98_livetracking: added module
   - bugfix:  38_netatmo: changed geocoding to openstreetmap
   - bugfix:  88_HMCCU: support for CCU firmware 3.41.7
   - bugfix:  38_netatmo: fixed ignored values after 'dead' state
diff --git a/fhem/FHEM/98_livetracking.pm b/fhem/FHEM/98_livetracking.pm
new file mode 100644
index 000000000..e6e7f911d
--- /dev/null
+++ b/fhem/FHEM/98_livetracking.pm
@@ -0,0 +1,1446 @@
+##############################################
+# $Id$$$ 2018-11-01
+#
+#  98_livetracking.pm
+#
+#  2018 Markus Moises < vorname at nachname . de >
+#
+#  This module provides livetracking data from OwnTracks, OpenPaths and Swarm (FourSquare)
+#
+#
+##############################################################################
+#
+# define <name> livetracking <openpaths_key> <openpaths_secret> <swarm_token>
+#
+##############################################################################
+
+package main;
+
+use strict;
+use warnings;
+use Time::Local;
+
+#use Math::Round;
+#use Net::OAuth;
+
+use JSON;
+use URI::Escape;
+use Data::Dumper;
+
+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 ".
+                                "traccarDevice ".
+                                "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 ".
+                                $readingFnAttributes;
+
+
+}
+
+sub livetracking_Define($$$) {
+  my ($hash, $def) = @_;
+  my @a = split("[ \t][ \t]*", $def);
+
+  return "syntax: define <name> livetracking <openpaths_key> <openpaths_secret> <swarm_token>" 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});
+  }
+  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});
+    $hash->{helper}{swarm_token} = $a[4];
+  }
+
+
+  my $req = eval
+  {
+    require XML::Simple;
+    XML::Simple->import();
+    1;
+  };
+
+  if($req)
+  {
+    $hash->{NOTIFYDEV} = AttrVal($name, "owntracksDevice" , AttrVal($name, "traccarDevice" , "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';
+  }
+
+  #$hash->{STATE} = "Initialized";
+
+  return undef;
+}
+
+sub livetracking_Undefine($$) {
+  my ($hash, $arg) = @_;
+  RemoveInternalTimer($hash);
+  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";
+  }
+  else{
+    $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 !~ /</ || $messagetext !~ />/);
+    if($messagetext eq "")
+    {
+      $messagetext = '';
+    }
+    elsif($messagetext !~ /</ || $messagetext !~ />/)
+    {
+      $messagetext = '"content":"'.FmtDateTime(time()).'<br/>FHEM: <br/><br/>'.$messagetext.'",';
+    }
+
+    fhem('set '.$devname.' msg {"_type":"cmd","action":"action",'.$messagetext.$notifytext.'"tst":'.time().'}');
+    #fhem('set '.$devname.' msg {"_type":"cmd","action":"notify", "content":"'.$notifytext.'","tst":'.time().'}') if($notifytext ne "");
+  }
+
+  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 OpenPaths:noArg Swarm:noArg";
+  $usage .= " owntracksLocation:noArg owntracksSteps:noArg" if(defined($attr{$name}{owntracksDevice}));
+  $usage .= " address";
+
+  return $usage if $command eq '?';
+
+  RemoveInternalTimer($hash);
+
+  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"));
+     if($parameter =~ /,/){
+       @location = split(",",$parameter);
+     }
+     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&limit=1",
+         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}) ) {
+         return $json->{display_name};
+       } elsif( defined($json->{address}) ) {
+         my $addr = "";
+         $addr .= $json->{address}->{road}." " if(defined($json->{address}->{road}));
+         $addr .= $json->{address}->{bridleway}." " if(defined($json->{address}->{bridleway}) && !defined($json->{address}->{road}));
+         $addr .= $json->{address}->{footway}." " if(defined($json->{address}->{footway}) && !defined($json->{address}->{road}) && !defined($json->{address}->{bridleway}));
+         $addr .= $json->{address}->{house_number} if(defined($json->{address}->{house_number}));
+         $addr .= "\n".$json->{address}->{neighbourhood} if(defined($json->{address}->{neighbourhood}) && $parameter eq "long");
+         $addr .= "\n".$json->{address}->{suburb} if(defined($json->{address}->{suburb}) && $parameter eq "long");
+         $addr .= "\n" if(defined($json->{address}->{postcode}) || defined($json->{address}->{city}) || defined($json->{address}->{town}));
+         $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 .= "\n".$json->{address}->{county} if(defined($json->{address}->{county}) && $parameter eq "long");
+         $addr .= "\n" if((defined($json->{address}->{state_district}) || defined($json->{address}->{state})) && $parameter eq "long");
+         $addr .= $json->{address}->{state_district}." " if(defined($json->{address}->{state_district}) && $parameter eq "long");
+         $addr .= $json->{address}->{state} if(defined($json->{address}->{state}) && $parameter eq "long");
+         $addr .= "\n".$json->{address}->{country} if(defined($json->{address}->{country}));
+         #Log3 ($name, 3, "$name: ".Dumper($json));
+         return $addr;
+       } elsif( defined($json->{display_name}) ) {
+         return $json->{display_name};
+       } else {
+         return "no data";
+       }
+     } else {
+       return "invalid coordinates";
+     }
+     return undef;
+  }
+
+
+
+  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;
+  }
+  if ($attr && $attr eq 'traccarDevice') {
+    $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.".*" );
+  }
+  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}));
+
+  return undef;
+}
+
+
+sub livetracking_GetOpenPaths($) {
+  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($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_GetAll", $hash, 0);
+  $hash->{UPDATED} = FmtDateTime(time());
+
+  return undef;
+}
+
+
+
+sub livetracking_GetSwarm($) {
+  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($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", 1800);
+  #RemoveInternalTimer($hash);
+  InternalTimer( gettimeofday() + $interval, "livetracking_GetAll", $hash, 0);
+  $hash->{UPDATED} = FmtDateTime(time());
+
+  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;
+
+
+    #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
+    #push(@{$hash->{CHANGETIME}}, 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')
+    {
+      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)
+      {
+        #$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
+  }
+
+
+
+  if($updated == 1)
+  {
+    #readingsSingleUpdate($hash,"lastOpenPaths",$lastreading,1);
+    #$hash->{CHANGED} = ();
+    #$hash->{CHANGETIME} = ();
+    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 = $dataset->{venue}->{location}->{lat}.",".$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;
+    }
+
+    #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
+    #push(@{$hash->{CHANGETIME}}, 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;
+
+    #$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_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));
+
+    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/ ))
+    {
+      Log3 ($name, 5, "WRONG OWNTRACKS TYPE ".Dumper($dev->{CHANGED}[0]));
+      return undef;
+    }
+
+    $data= substr($dev->{CHANGED}[0],index($dev->{CHANGED}[0], ": {")+2);
+    $dataset = JSON->new->utf8(0)->decode($data);
+
+  } elsif($devName eq AttrVal($name, "traccarDevice" , "traccar")) {
+    if(!($dev->{CHANGED}[0] =~ m/position":[ ]?{/))
+    {
+      Log3 ($name, 5, "WRONG TRACCAR TYPE ".Dumper($dev->{CHANGED}[0]));
+      return undef;
+    }
+
+    $data= substr($dev->{CHANGED}[0],index($dev->{CHANGED}[0], ": {")+2);
+    my $traccardata = JSON->new->utf8(0)->decode($data);
+
+  } else {
+    Log3 ($name, 5, "livetracks: Notify ignored from ".$devName);
+    return undef;
+  }
+
+
+
+
+  if($dev->{CHANGED}[0] =~ 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($dev->{CHANGED}[0] =~ 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", $dataset->{prox});
+        $hash->{CHANGETIME}[1] = FmtDateTime($dataset->{tst});
+        readingsBulkUpdate($hash, "beacon_".$i."_accuracy", $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;
+  }
+
+
+  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);
+
+  #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
+  #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
+  if($accurate)
+  {
+    readingsBulkUpdate($hash, "location", $dataset->{lat}.",".$dataset->{lon});
+    $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
+  }
+  else
+  {
+    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)
+  {
+    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" );
+  #}
+  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" );
+  #}
+  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}))
+  {
+    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", $dataset->{p}*10);
+    $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
+  }
+  if(defined($dataset->{desc}) and defined($dataset->{event}))
+  {
+    Log3 ($name, 3, "$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")
+    {
+      #$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
+    {
+      #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);
+      }
+    }
+  }
+  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);
+
+  $hash->{helper}{lastOwnTracks} = $dataset->{tst};
+
+  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 "[]");
+      return undef;
+    }
+
+    my $json;
+    $json = JSON->new->utf8(0)->decode($data);
+
+
+    if( $param->{type} eq 'openpathsdata' ) {
+      livetracking_ParseOpenPaths($hash,$json);
+    } elsif( $param->{type} eq 'swarmdata' ) {
+      livetracking_ParseSwarm($hash,$json);
+    }
+  }
+}
+
+
+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_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 = "";
+  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, OpenPaths and Swarm
+=begin html
+
+<a name="livetracking"></a>
+<h3>livetracking</h3>
+<ul>
+  This modul provides livetracking data from OpenPaths and Swarm (FourSquare).<br/>
+  Swarm Token: https://foursquare.com/oauth2/authenticate?client_id=EFWJ0DNVIREJ2CY1WDIFQ4MAL0ZGZAZUYCNE0NE0XZC3NCPX&response_type=token&redirect_uri=http://localhost&display=wap
+  <br/><br/>
+
+  <b>Define</b>
+  <ul>
+    <code>define &lt;name&gt; livetracking &lt;...&gt;</code>
+    <br>
+    Example: <code>define livetrackingdata livetracking [openpaths_key] [openpaths_secret] [swarm_token]</code><br/>
+    Either both, just OpenPaths, just Swarm or none of them can be defined.
+    <br>&nbsp;
+    <li><code>...</code>
+      <br>
+      ...
+    </li><br>
+  </ul>
+
+  <br>
+  <b>Get</b>
+   <ul>
+      <li><code>All/OpenPaths/Swarm</code>
+      <br>
+      Manually trigger a data update for OpenPaths/Swarm
+      </li><br>
+      <li><code>owntracksLocation</code>
+      <br>
+      Request position from OwnTracks
+      </li><br>
+      <li><code>owntracksSteps</code>
+      <br>
+      Request steps data from OwnTracks
+      </li><br>
+      <li><code>address [short/long/lat,lng]</code>
+      <br>
+      Get address from coordinates
+      </li><br>
+  </ul>
+
+  <br>
+  <b>Set</b>
+   <ul>
+      <li><code>owntracksMessage</code>
+      <br>
+      Send a message to OwnTracks
+      </li><br>
+  </ul>
+
+  <br>
+  <b>Readings</b>
+   <ul>
+      <li><code>location</code>
+      <br>
+      GPS position
+      </li><br>
+      <li><code>distance</code> km
+      <br>
+      GPS distance from home
+      </li><br>
+      <li><code>accuracy</code> m
+      <br>
+      GPS accuracy
+      </li><br>
+      <li><code>altitude</code> m
+      <br>
+      GPS altitude
+      </li><br>
+      <li><code>velocity</code> km/h
+      <br>
+      GPS velocity
+      </li><br>
+      <li><code>heading</code> deg
+      <br>
+      GPS heading
+      </li><br>
+      <li><code>place</code>
+      <br>
+      Swarm place name
+      </li><br>
+      <li><code>steps</code> steps
+      <br>
+      iOS walked steps
+      </li><br>
+      <li><code>walking</code> m
+      <br>
+      iOS walked distance
+      </li><br>
+      <li><code>floorsup</code> floors
+      <br>
+      iOS floors walked up
+      </li><br>
+      <li><code>floorsdown</code> floors
+      <br>
+      iOS floors walked down
+      </li><br>
+      <li><code>zone_N</code> active/inactive
+      <br>
+      Zone status in OwnTracks
+      </li><br>
+      <li><code>beacon</code>
+      <br>
+      Beacon ID from OwnTracks
+      </li><br>
+      <li><code>beacon_N_X</code>
+      <br>
+      Beacon data for saved beacons for indoor positioning
+      </li><br>
+      <li><code>batteryState</code> ok/low
+      <br>
+      Battery state (can be set through attribute batteryWarning )
+      </li><br>
+      <li><code>batteryPercent</code> %
+      <br>
+      Battery percentage
+      </li><br>
+  </ul>
+
+
+  <br>
+   <b>Attributes</b>
+   <ul>
+      <li><code>batteryWarning</code>
+         <br>
+         Set battery ok/low threshold
+      </li><br>
+      <li><code>beacon_N</code>
+         <br>
+         Saved beacon IDs from OwnTracks for indoor positioning, e.g.:<br/>
+         FDA50693-A4E2-4FB1-AFCF-C6EB07647825,19789,1
+      </li><br>
+      <li><code>zonename_N</code>
+         <br>
+         Assign zone name from OwnTracks
+      </li><br>
+      <li><code>home</code>
+         <br>
+         Home location (lat,lon)
+      </li><br>
+      <li><code>swarmHome</code>
+         <br>
+         Fake home location (that is assigned to private homes for security reasons) of your Swarm home (lat,lon)
+      </li><br>
+      <li><code>filterAccuracy</code>
+         <br>
+         Minimum accuracy of GPS location to update any readings
+      </li><br>
+      <li><code>roundDistance, roundAltitude</code>
+         <br>
+         Rounding for distance and altitude readings to prevent too many changes
+      </li><br>
+      <li><code>owntracksDevice</code>
+         <br>
+         OwnTracks MQTT device to look for notifies from
+      </li><br>
+
+  </ul>
+</ul>
+=end html
+=cut