98_livetracking.pm 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696
  1. ##############################################
  2. # $Id: 98_livetracking.pm 17717 2018-11-09 22:26:18Z moises $$$ 2018-11-01
  3. #
  4. # 98_livetracking.pm
  5. #
  6. # 2018 Markus Moises < vorname at nachname . de >
  7. #
  8. # This module provides livetracking data from OwnTracks, OpenPaths and Swarm (FourSquare)
  9. #
  10. #
  11. ##############################################################################
  12. #
  13. # define <name> livetracking <openpaths_key> <openpaths_secret> <swarm_token>
  14. #
  15. ##############################################################################
  16. package main;
  17. use strict;
  18. use warnings;
  19. no warnings qw(redefine);
  20. #use Math::Round;
  21. #use Net::OAuth;
  22. use JSON;
  23. use Time::Local;
  24. use URI::Escape;
  25. use Data::Dumper;
  26. use Encode qw(encode_utf8 decode_utf8);
  27. use utf8;
  28. my $libcheck_hasOAuth = 1;
  29. ##############################################################################
  30. sub livetracking_Initialize($) {
  31. my ($hash) = @_;
  32. my $name = $hash->{NAME};
  33. eval "use Net::OAuth;";
  34. $libcheck_hasOAuth = 0 if($@);
  35. $hash->{DefFn} = "livetracking_Define";
  36. $hash->{UndefFn} = "livetracking_Undefine";
  37. $hash->{GetFn} = "livetracking_Get";
  38. $hash->{SetFn} = "livetracking_Set";
  39. $hash->{AttrFn} = "livetracking_Attr";
  40. $hash->{NotifyFn} = "livetracking_Notify";
  41. $hash->{NotifyOrderPrefix}= "999-";
  42. $hash->{DbLog_splitFn} = "livetracking_DbLog_splitFn";
  43. $hash->{AttrList} = "disable:1 ".
  44. "roundAltitude ".
  45. "roundDistance ".
  46. "filterAccuracy ".
  47. "interval ".
  48. "home ".
  49. "swarmHome ".
  50. "owntracksDevice ".
  51. "beacon_0 ".
  52. "beacon_1 ".
  53. "beacon_2 ".
  54. "beacon_3 ".
  55. "beacon_4 ".
  56. "beacon_5 ".
  57. "beacon_6 ".
  58. "beacon_7 ".
  59. "beacon_8 ".
  60. "beacon_9 ".
  61. "zonename_0 ".
  62. "zonename_1 ".
  63. "zonename_2 ".
  64. "zonename_3 ".
  65. "zonename_4 ".
  66. "zonename_5 ".
  67. "zonename_6 ".
  68. "zonename_7 ".
  69. "zonename_8 ".
  70. "zonename_9 ".
  71. "batteryWarning:5,10,15,20,25,30,35,40 ".
  72. "addressLanguage:de,en,fr,es,it,nl ".
  73. "addressReading:0,1 ".
  74. "osmandServer:0,1 ".
  75. "osmandId ".
  76. $readingFnAttributes;
  77. }
  78. sub livetracking_Define($$$) {
  79. my ($hash, $def) = @_;
  80. my @a = split("[ \t][ \t]*", $def);
  81. return "syntax: define <name> livetracking <openpaths_key> <openpaths_secret> <swarm_token>" if(int(@a) < 2 || int(@a) > 7 );
  82. my $name = $hash->{NAME};
  83. #$hash->{OAuth_exists} = $libcheck_hasOAuth if($libcheck_hasOAuth);
  84. if(int(@a) == 4 ) {
  85. $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists});
  86. $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists});
  87. }
  88. elsif(int(@a) == 3 ) {
  89. $hash->{helper}{swarm_token} = $a[2];
  90. }
  91. elsif(int(@a) == 5 ) {
  92. $hash->{helper}{openpaths_key} = $a[2];# if($hash->{OAuth_exists});
  93. $hash->{helper}{openpaths_secret} = $a[3];# if($hash->{OAuth_exists});
  94. $hash->{helper}{swarm_token} = $a[4];
  95. }
  96. my $req = eval
  97. {
  98. require XML::Simple;
  99. XML::Simple->import();
  100. 1;
  101. };
  102. if($req)
  103. {
  104. $hash->{NOTIFYDEV} = AttrVal($name, "owntracksDevice" , "owntracks");
  105. }
  106. else
  107. {
  108. $hash->{STATE} = "XML::Simple is required!";
  109. $attr{$name}{disable} = "1";
  110. return undef;
  111. }
  112. # my $resolve = inet_aton("api.foursquare.com");
  113. # if(!defined($resolve) && defined($hash->{helper}{swarm_token}))
  114. # {
  115. # $hash->{STATE} = "DNS error";
  116. # InternalTimer( gettimeofday() + 1800, "livetracking_GetAll", $hash, 0);
  117. # return undef;
  118. # }
  119. InternalTimer( gettimeofday() + 60, "livetracking_GetSwarm", $hash, 0) if(defined($hash->{helper}{swarm_token}));
  120. # $resolve = inet_aton("openpaths.cc");
  121. # if(!defined($resolve) && defined($hash->{helper}{openpaths_key}))
  122. # {
  123. # $hash->{STATE} = "DNS error";
  124. # InternalTimer( gettimeofday() + 1800, "livetracking_GetAll", $hash, 0);
  125. # return undef;
  126. # }
  127. InternalTimer( gettimeofday() + 90, "livetracking_GetOpenPaths", $hash, 0) if(defined($hash->{helper}{openpaths_key}));
  128. if (!defined($attr{$name}{stateFormat}))
  129. {
  130. $attr{$name}{stateFormat} = 'location';
  131. }
  132. #$hash->{STATE} = "Initialized";
  133. return undef;
  134. }
  135. sub livetracking_Undefine($$) {
  136. my ($hash, $arg) = @_;
  137. RemoveInternalTimer($hash);
  138. return undef;
  139. }
  140. sub livetracking_Set($$@) {
  141. my ($hash, $name, $command, @parameters) = @_;
  142. my $usage = "Unknown argument $command, choose one of";
  143. if(defined($attr{$name}{owntracksDevice}))
  144. {
  145. $usage .= " owntracksMessage";
  146. }
  147. else{
  148. $usage = undef;
  149. }
  150. return $usage if $command eq '?';
  151. my $devname=AttrVal($name, "owntracksDevice" , "owntracks" );
  152. if($command eq 'owntracksMessage') {
  153. my $messagetext = join( ' ', @parameters );
  154. my $notifytext = '';
  155. $notifytext = '"notify":"FHEM: ' . join( ' ', @parameters ).'",' if($messagetext !~ /</ || $messagetext !~ />/);
  156. if($messagetext eq "")
  157. {
  158. $messagetext = '';
  159. }
  160. elsif($messagetext !~ /</ || $messagetext !~ />/)
  161. {
  162. $messagetext = '"content":"'.FmtDateTime(time()).'<br/>FHEM: <br/><br/>'.$messagetext.'",';
  163. }
  164. fhem('set '.$devname.' cmd {"_type":"cmd","action":"action",'.$messagetext.$notifytext.'"tst":'.time().'}');
  165. #fhem('set '.$devname.' msg {"_type":"cmd","action":"notify", "content":"'.$notifytext.'","tst":'.time().'}') if($notifytext ne "");
  166. }
  167. return undef;
  168. }
  169. sub livetracking_Get($@) {
  170. my ($hash, @a) = @_;
  171. my $command = $a[1];
  172. my $parameter = $a[2];# if(defined($a[2]));
  173. my $name = $hash->{NAME};
  174. my $usage = "Unknown argument $command, choose one of All:noArg OpenPaths:noArg Swarm:noArg";
  175. $usage .= " owntracksLocation:noArg owntracksSteps:noArg" if(defined($attr{$name}{owntracksDevice}));
  176. $usage .= " address";
  177. return $usage if $command eq '?';
  178. if(AttrVal($name, "disable", 0) eq 1)
  179. {
  180. return "livetracking $name is disabled. Aborting...";
  181. }
  182. my $devname = AttrVal($name, "owntracksDevice" , "owntracks" );
  183. if($command eq "All")
  184. {
  185. livetracking_GetAll($hash);
  186. }
  187. elsif($command eq "OpenPaths")
  188. {
  189. livetracking_GetOpenPaths($hash);
  190. }
  191. elsif($command eq "Swarm")
  192. {
  193. livetracking_GetSwarm($hash);
  194. }
  195. elsif($command eq 'owntracksLocation') {
  196. fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportLocation"}');
  197. return undef;
  198. }
  199. elsif($command eq 'owntracksSteps') {
  200. fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportSteps"}');
  201. return undef;
  202. }
  203. elsif($command eq 'address') {
  204. my @location = split(",",ReadingsVal($name,"location","0,0"));
  205. $parameter = "" if(!defined($parameter));
  206. if($parameter =~ /,/){
  207. @location = split(",",$parameter);
  208. }
  209. my $lang = AttrVal($name,"addressLanguage","en");
  210. if(defined($location[1])) {
  211. my($err,$data) = HttpUtils_BlockingGet({
  212. url => "https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=".$location[0]."&lon=".$location[1]."&addressdetails=1&accept-language=$lang",
  213. noshutdown => 1,
  214. });
  215. return "data error" if($err);
  216. return "invalid json" if( $data !~ m/^{.*}$/ && $data !~ m/^\[.*\]$/ );
  217. my $json = eval { JSON->new->utf8(0)->decode($data) };
  218. return "invalid json evaluation" if($@);
  219. if( $parameter eq "short" && defined($json->{display_name}) ) {
  220. readingsSingleUpdate($hash,"address",livetracking_utf8clean($json->{display_name}),1) if(AttrVal($name,"addressReading",0));
  221. return livetracking_utf8clean($json->{display_name});
  222. } elsif( defined($json->{address}) ) {
  223. my $addr = "";
  224. if($parameter eq "long"){
  225. $addr .= $json->{address}->{housename}."\n" if(defined($json->{address}->{housename}));
  226. $addr .= $json->{address}->{parking}."\n" if(defined($json->{address}->{parking}));
  227. }
  228. $addr .= $json->{address}->{road}." " if(defined($json->{address}->{road}));
  229. $addr .= $json->{address}->{path}." " if(defined($json->{address}->{path}) && !defined($json->{address}->{road}));
  230. $addr .= $json->{address}->{bridleway}." " if(defined($json->{address}->{bridleway}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}));
  231. $addr .= $json->{address}->{footway}." " if(defined($json->{address}->{footway}) && !defined($json->{address}->{road}) && !defined($json->{address}->{path}) && !defined($json->{address}->{bridleway}));
  232. $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}));
  233. $addr .= $json->{address}->{house_number} if(defined($json->{address}->{house_number}));
  234. #$addr .= "\n".$json->{address}->{neighbourhood} if(defined($json->{address}->{neighbourhood}) && $parameter eq "long");
  235. if($parameter eq "long"){
  236. $addr .= "\n".$json->{address}->{suburb} if(defined($json->{address}->{suburb}));
  237. }
  238. $addr .= "\n" if(defined($json->{address}->{postcode}) || defined($json->{address}->{city}) || defined($json->{address}->{town}) || defined($json->{address}->{village}));
  239. $addr .= $json->{address}->{postcode}." " if(defined($json->{address}->{postcode}));
  240. $addr .= $json->{address}->{city} if(defined($json->{address}->{city}));
  241. $addr .= $json->{address}->{town}." " if(defined($json->{address}->{town}) && !defined($json->{address}->{city}));
  242. $addr .= $json->{address}->{village}." " if(defined($json->{address}->{village}) && !defined($json->{address}->{city}) && !defined($json->{address}->{town}));
  243. if($parameter eq "long"){
  244. $addr .= "\n".$json->{address}->{county} if(defined($json->{address}->{county}));
  245. $addr .= "\n" if((defined($json->{address}->{state_district}) || defined($json->{address}->{state})));
  246. $addr .= $json->{address}->{state_district}." " if(defined($json->{address}->{state_district}));
  247. $addr .= $json->{address}->{state} if(defined($json->{address}->{state}));
  248. }
  249. $addr .= "\n".$json->{address}->{country} if(defined($json->{address}->{country}));
  250. Log3 ($name, 4, "$name: address received\n".Dumper($json));
  251. readingsSingleUpdate($hash,"address",livetracking_utf8clean($addr),1) if(AttrVal($name,"addressReading",0));
  252. return livetracking_utf8clean($addr);
  253. } elsif( defined($json->{display_name}) ) {
  254. readingsSingleUpdate($hash,"address",livetracking_utf8clean($json->{display_name}),1) if(AttrVal($name,"addressReading",0));
  255. return livetracking_utf8clean($json->{display_name});
  256. } else {
  257. return "no data";
  258. }
  259. } else {
  260. return "invalid coordinates";
  261. }
  262. return undef;
  263. }
  264. return undef;
  265. }
  266. sub livetracking_Attr(@) {
  267. my ($command, $name, $attr, $val) = @_;
  268. my $hash = $defs{$name};
  269. if ($attr && $attr eq 'owntracksDevice') {
  270. $hash->{NOTIFYDEV} = $val if defined $val;
  271. }
  272. elsif ($attr && $attr =~ /^(zonename_)([0-9]+)/) {
  273. fhem( "deletereading $name zone_".$2 );
  274. }
  275. elsif ($attr && $attr =~ /^(beacon_)([0-9]+)/) {
  276. fhem( "deletereading $name beacon_".$2.".*" );
  277. }
  278. elsif ($attr && $attr eq 'osmandServer') {
  279. if($command eq "set" && $val == 1){
  280. livetracking_addExtension($hash);
  281. } else {
  282. livetracking_removeExtension($hash);
  283. }
  284. }
  285. return undef;
  286. }
  287. sub livetracking_GetAll($) {
  288. my ($hash) = @_;
  289. my $name = $hash->{NAME};
  290. RemoveInternalTimer($hash);
  291. if(AttrVal($name, "disable", 0) eq 1)
  292. {
  293. Log3 ($name, 4, "livetracking $name is disabled, data update cancelled.");
  294. return undef;
  295. }
  296. if(defined($attr{$name}{owntracksDevice}))
  297. {
  298. my $devname=AttrVal($name, "owntracksDevice" , "owntracks" );
  299. fhem('set '.$devname.' cmd {"_type":"cmd","action":"reportLocation"}');
  300. }
  301. # my $resolve = inet_aton("api.foursquare.com");
  302. # if(!defined($resolve) && defined($hash->{helper}{swarm_token}))
  303. # {
  304. # $hash->{STATE} = "DNS error";
  305. # InternalTimer( gettimeofday() + 3600, "livetracking_GetAll", $hash, 0);
  306. # return undef;
  307. # }
  308. InternalTimer( gettimeofday() + 5, "livetracking_GetSwarm", $hash, 0) if(defined($hash->{helper}{swarm_token}));
  309. # $resolve = inet_aton("openpaths.cc");
  310. # if(!defined($resolve) && defined($hash->{helper}{openpaths_key}))
  311. # {
  312. # $hash->{STATE} = "DNS error";
  313. # InternalTimer( gettimeofday() + 3600, "livetracking_GetAll", $hash, 0);
  314. # return undef;
  315. # }
  316. InternalTimer( gettimeofday() + 10, "livetracking_GetOpenPaths", $hash, 0) if(defined($hash->{helper}{openpaths_key}));
  317. return undef;
  318. }
  319. sub livetracking_GetOpenPaths($) {
  320. my ($hash) = @_;
  321. my $name = $hash->{NAME};
  322. #RemoveInternalTimer($hash);
  323. RemoveInternalTimer($hash, "livetracking_GetOpenPaths");
  324. if(AttrVal($name, "disable", 0) eq 1)
  325. {
  326. Log3 ($name, 4, "livetracking $name is disabled, data update cancelled.");
  327. return undef;
  328. }
  329. if(!defined($hash->{helper}{openpaths_key}))
  330. {
  331. return undef;
  332. }
  333. my $nonce = "";
  334. for (my $i=0;$i<32;$i++) {
  335. my $r = int(rand(62));
  336. if ($r<10) { $r += 48; }
  337. elsif ($r<36) { $r += 55; }
  338. else { $r += 61; }
  339. $nonce .= chr($r);
  340. }
  341. my $request = Net::OAuth->request("request token")->new(
  342. consumer_key => $hash->{helper}{openpaths_key},
  343. consumer_secret => $hash->{helper}{openpaths_secret},
  344. request_url => 'https://openpaths.cc/api/1',
  345. request_method => 'GET',
  346. signature_method => 'HMAC-SHA1',
  347. timestamp => livetracking_roundfunc(time()),
  348. nonce => $nonce,
  349. );
  350. $request->sign;
  351. my $lastupdate = livetracking_roundfunc(ReadingsVal($name,".lastOpenPaths",time()-3600));
  352. my $url = $request->to_url."&start_time=".$lastupdate."&num_points=50"; # start_time/end_time/num_points
  353. Log3 ($name, 4, "livetracking OpenPaths URL: ".$url);
  354. HttpUtils_NonblockingGet({
  355. url => $url,
  356. timeout => 10,
  357. noshutdown => 1,
  358. hash => $hash,
  359. type => 'openpathsdata',
  360. callback => \&livetracking_dispatch,
  361. });
  362. my $interval = AttrVal($hash->{NAME}, "interval", 1800);
  363. #RemoveInternalTimer($hash);
  364. InternalTimer( gettimeofday() + $interval, "livetracking_GetOpenPaths", $hash, 0);
  365. $hash->{UPDATED} = FmtDateTime(time());
  366. return undef;
  367. }
  368. sub livetracking_GetSwarm($) {
  369. my ($hash) = @_;
  370. my $name = $hash->{NAME};
  371. #RemoveInternalTimer($hash);
  372. RemoveInternalTimer($hash, "livetracking_GetSwarm");
  373. if(AttrVal($name, "disable", 0) eq 1)
  374. {
  375. Log3 ($name, 4, "livetracking $name is disabled, data update cancelled.");
  376. return undef;
  377. }
  378. if(!defined($hash->{helper}{swarm_token}))
  379. {
  380. return undef;
  381. }
  382. my $lastupdate = livetracking_roundfunc(ReadingsVal($name,".lastSwarm",time()-3600));
  383. my $url = "https://api.foursquare.com/v2/users/self/checkins?oauth_token=".$hash->{helper}{swarm_token}."&v=20150516&sort=oldestfirst&limit=25&afterTimestamp=".$lastupdate;
  384. HttpUtils_NonblockingGet({
  385. url => $url,
  386. timeout => 10,
  387. noshutdown => 1,
  388. hash => $hash,
  389. type => 'swarmdata',
  390. callback => \&livetracking_dispatch,
  391. });
  392. my $interval = AttrVal($hash->{NAME}, "interval", 1800);
  393. #RemoveInternalTimer($hash);
  394. InternalTimer( gettimeofday() + $interval, "livetracking_GetSwarm", $hash, 0);
  395. $hash->{UPDATED} = FmtDateTime(time());
  396. return undef;
  397. }
  398. sub livetracking_ParseOpenPaths($$) {
  399. my ($hash,$json) = @_;
  400. my $name = $hash->{NAME};
  401. my $updated = 0;
  402. my $lastreading = ReadingsVal($name,".lastOpenPaths",time()-300);
  403. my $device = ReadingsVal($name,"deviceOpenPaths","");
  404. my $os = ReadingsVal($name,"osOpenPaths","");
  405. my $version = ReadingsVal($name,"versionOpenPaths","");
  406. my $altitude = ReadingsVal($name,"altitude","0");
  407. my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1);
  408. Log3 ($name, 6, "$name OpenPaths data: /n".Dumper($json));
  409. foreach my $dataset (@{$json})
  410. {
  411. Log3 ($name, 5, "$name OpenPaths: at ".FmtDateTime($dataset->{t})." / ".$dataset->{lat}.",".$dataset->{lon});
  412. $lastreading = $dataset->{t}+1;
  413. readingsBeginUpdate($hash); # Begin update readings
  414. $hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  415. my $changeindex = 0;
  416. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  417. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  418. readingsBulkUpdate($hash, "location", $dataset->{lat}.",".$dataset->{lon});
  419. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  420. #setReadingsVal($hash, "location", $dataset->{lat}.",".$dataset->{lon}, FmtDateTime($dataset->{t}));
  421. #push(@{$hash->{CHANGED}}, "location: ".$dataset->{lat}.",".$dataset->{lon});
  422. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  423. if(defined($dataset->{alt}) && $dataset->{alt} ne '0')
  424. {
  425. my $newaltitude = livetracking_roundfunc($dataset->{alt}/$altitudeRound)*$altitudeRound;
  426. #Log3 ($name, 0, "$name SwarmRound: ".$dataset->{alt}."/".$altitudeRound." = ".livetracking_roundfunc($dataset->{alt}/$altitudeRound)." *".$altitudeRound);
  427. if($altitude ne $newaltitude)
  428. {
  429. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  430. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  431. readingsBulkUpdate($hash, "altitude", $newaltitude);
  432. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  433. #setReadingsVal($hash, "altitude", $newaltitude." m", FmtDateTime($dataset->{t}));
  434. #push(@{$hash->{CHANGED}}, "altitude: ".$newaltitude." m");
  435. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  436. $altitude = $newaltitude;
  437. }
  438. }
  439. if(defined($dataset->{device}) && $dataset->{device} ne $device)
  440. {
  441. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  442. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  443. readingsBulkUpdate($hash, "deviceOpenPaths", $dataset->{device});
  444. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  445. #setReadingsVal($hash, "deviceOpenPaths", $dataset->{device}, FmtDateTime($dataset->{t}));
  446. #push(@{$hash->{CHANGED}}, "deviceOpenPaths: ".$dataset->{device});
  447. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  448. }
  449. if(defined($dataset->{os}) && $dataset->{os} ne $os)
  450. {
  451. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  452. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  453. readingsBulkUpdate($hash, "osOpenPaths", $dataset->{os});
  454. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  455. #setReadingsVal($hash, "osOpenPaths", $dataset->{os}, FmtDateTime($dataset->{t}));
  456. #push(@{$hash->{CHANGED}}, "osOpenPaths: ".$dataset->{os});
  457. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  458. }
  459. if(defined($dataset->{version}) && $dataset->{version} ne $version)
  460. {
  461. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  462. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  463. readingsBulkUpdate($hash, "versionOpenPaths", $dataset->{version});
  464. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  465. #setReadingsVal($hash, "versionOpenPaths", $dataset->{version}, FmtDateTime($dataset->{t}));
  466. #push(@{$hash->{CHANGED}}, "versionOpenPaths: ".$dataset->{version});
  467. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  468. }
  469. if(defined($attr{$name}{home}))
  470. {
  471. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{t});
  472. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  473. readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home}));
  474. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  475. #setReadingsVal($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km", FmtDateTime($dataset->{t}));
  476. #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km");
  477. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{t}));
  478. }
  479. $updated = 1;
  480. #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{t});
  481. readingsEndUpdate($hash, 1); # End update readings
  482. }
  483. if($updated == 1)
  484. {
  485. #readingsSingleUpdate($hash,"lastOpenPaths",$lastreading,1);
  486. #$hash->{CHANGED} = ();
  487. #$hash->{CHANGETIME} = ();
  488. readingsSingleUpdate($hash,".lastOpenPaths",$lastreading,1);
  489. $hash->{helper}{lastOpenPaths} = $lastreading;
  490. }
  491. return undef;
  492. }
  493. sub livetracking_ParseSwarm($$) {
  494. my ($hash,$json) = @_;
  495. my $name = $hash->{NAME};
  496. my $updated = 0;
  497. my $lastreading = ReadingsVal($name,".lastSwarm",time()-300);
  498. my $device = ReadingsVal($name,"deviceSwarm","");
  499. Log3 ($name, 6, "$name Swarm data: /n".Dumper($json));
  500. foreach my $dataset (@{$json->{response}->{checkins}->{items}})
  501. {
  502. next if(!defined($dataset->{type}) || $dataset->{type} ne "checkin");
  503. readingsBeginUpdate($hash);
  504. $hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
  505. my $changeindex = 0;
  506. $lastreading = $dataset->{createdAt}+1;
  507. my $place = livetracking_utf8clean($dataset->{venue}->{name});
  508. Log3 ($name, 4, "$name Swarm: ".$place." at ".FmtDateTime($dataset->{createdAt})." / ".$dataset->{venue}->{location}->{lat}.",".$dataset->{venue}->{location}->{lng});
  509. my $loc = $dataset->{venue}->{location}->{lat}.",".$dataset->{venue}->{location}->{lng};
  510. if(defined($attr{$name}{swarmHome}) and defined($attr{$name}{home}))
  511. {
  512. my $shl = $attr{$name}{swarmHome};
  513. my $home = $attr{$name}{home};
  514. $loc =~ s/$shl/$home/g;
  515. }
  516. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
  517. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  518. readingsBulkUpdate($hash, "location", $loc);
  519. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt});
  520. #setReadingsVal($hash, "location", $loc, FmtDateTime($dataset->{createdAt}));
  521. #push(@{$hash->{CHANGED}}, "location: ".$loc);
  522. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  523. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
  524. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  525. readingsBulkUpdate($hash, "place", $place);
  526. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt});
  527. #setReadingsVal($hash, "place", $dataset->{venue}->{name}, FmtDateTime($dataset->{createdAt}));
  528. #push(@{$hash->{CHANGED}}, "place: ".$dataset->{venue}->{name});
  529. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  530. if(defined($dataset->{source}->{name}) && $dataset->{source}->{name} ne $device)
  531. {
  532. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
  533. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  534. readingsBulkUpdate($hash, "deviceSwarm", $dataset->{source}->{name});
  535. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt});
  536. #setReadingsVal($hash, "deviceSwarm", $dataset->{source}->{name}, FmtDateTime($dataset->{createdAt}));
  537. #push(@{$hash->{CHANGED}}, "deviceSwarm: ".$dataset->{source}->{name});
  538. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  539. }
  540. if(defined($attr{$name}{home}))
  541. {
  542. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{createdAt});
  543. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  544. readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$loc,$attr{$name}{home})." km");
  545. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt});
  546. #setReadingsVal($hash, "distance", livetracking_distance($hash,$loc,$attr{$name}{home})." km", FmtDateTime($dataset->{createdAt}));
  547. #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$loc,$attr{$name}{home})." km");
  548. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{createdAt}));
  549. }
  550. $updated = 1;
  551. #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{createdAt});
  552. readingsEndUpdate($hash, 1);
  553. }
  554. if($updated == 1)
  555. {
  556. #readingsSingleUpdate($hash,"lastSwarm",$lastreading,1);
  557. #$hash->{CHANGED} = ();
  558. #$hash->{CHANGETIME} = ();
  559. readingsSingleUpdate($hash,".lastSwarm",$lastreading,1);
  560. $hash->{helper}{lastSwarm} = $lastreading;
  561. }
  562. return undef;
  563. }
  564. sub livetracking_Notify($$)
  565. {
  566. my ($hash, $dev) = @_;
  567. my $name = $hash->{NAME};
  568. my $devName = $dev->{NAME};
  569. my $dataset = "";
  570. my $data = "";
  571. # Ignore wrong notifications
  572. if($devName eq AttrVal($name, "owntracksDevice" , "owntracks"))
  573. {
  574. Log3 ($name, 6, "$name OwnTracks data: /n".Dumper($dev));
  575. my $invaliddata = 1;
  576. 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/ ))
  577. {
  578. $invaliddata = 0;#owntracks
  579. }
  580. elsif(($dev->{CHANGED}[0] =~ m/position":[ ]?{/))
  581. {
  582. $invaliddata = 0;#traccar
  583. }
  584. if($invaliddata == 1){
  585. Log3 ($name, 5, "WRONG MQTT TYPE ".Dumper($dev->{CHANGED}[0]));
  586. return undef;
  587. }
  588. $data= substr($dev->{CHANGED}[0],index($dev->{CHANGED}[0], ": {")+2);
  589. $dataset = JSON->new->utf8(0)->decode($data);
  590. } else {
  591. Log3 ($name, 5, "livetracks: Notify ignored from ".$devName);
  592. return undef;
  593. }
  594. if($dev->{CHANGED}[0] =~ m/_type":[ ]?"steps/)
  595. {
  596. readingsBeginUpdate($hash); # Start update readings
  597. $hash->{".updateTimestamp"} = FmtDateTime($dataset->{to});
  598. readingsBulkUpdate($hash, "steps", int($dataset->{steps}));
  599. $hash->{CHANGETIME}[0] = FmtDateTime($dataset->{to});
  600. readingsBulkUpdate($hash, "walking", int($dataset->{distance}));
  601. $hash->{CHANGETIME}[1] = FmtDateTime($dataset->{to});
  602. readingsBulkUpdate($hash, "floorsup", int($dataset->{floorsup}));
  603. $hash->{CHANGETIME}[2] = FmtDateTime($dataset->{to});
  604. readingsBulkUpdate($hash, "floorsdown", int($dataset->{floorsdown}));
  605. $hash->{CHANGETIME}[3] = FmtDateTime($dataset->{to});
  606. readingsEndUpdate($hash, 1);
  607. readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1);
  608. $hash->{helper}{lastOwnTracks} = $dataset->{tst};
  609. return undef;
  610. }
  611. if($dev->{CHANGED}[0] =~ m/_type":[ ]?"beacon/)
  612. {
  613. my $beaconid = $dataset->{uuid}.",".$dataset->{major}.",".$dataset->{minor};
  614. readingsBeginUpdate($hash); # Start update readings
  615. $hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  616. readingsBulkUpdate($hash, "beacon", $beaconid);
  617. $hash->{CHANGETIME}[0] = FmtDateTime($dataset->{tst});
  618. for(my $i=9;$i>=0;$i--)
  619. {
  620. next if(!defined($attr{$name}{"beacon_$i"}));
  621. if($beaconid eq $attr{$name}{"beacon_$i"})
  622. {
  623. readingsBulkUpdate($hash, "beacon_".$i."_proximity", $dataset->{prox});
  624. $hash->{CHANGETIME}[1] = FmtDateTime($dataset->{tst});
  625. readingsBulkUpdate($hash, "beacon_".$i."_accuracy", $dataset->{acc});
  626. $hash->{CHANGETIME}[2] = FmtDateTime($dataset->{tst});
  627. readingsBulkUpdate($hash, "beacon_".$i."_rssi", $dataset->{rssi});
  628. $hash->{CHANGETIME}[3] = FmtDateTime($dataset->{tst});
  629. last;
  630. }
  631. }
  632. readingsEndUpdate($hash, 1);
  633. readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1);
  634. $hash->{helper}{lastOwnTracks} = $dataset->{tst};
  635. return undef;
  636. }
  637. my $accurate = 1;
  638. $accurate = 0 if(defined($attr{$name}{filterAccuracy}) and defined($dataset->{acc}) and $attr{$name}{filterAccuracy} < $dataset->{acc});
  639. readingsBeginUpdate($hash); # Start update readings
  640. $hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  641. my $changeindex = 0;
  642. Log3 ($name, 4, "$name OwnTracks: ".FmtDateTime($dataset->{tst})." ".$data);
  643. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  644. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  645. if($accurate)
  646. {
  647. readingsBulkUpdate($hash, "location", $dataset->{lat}.",".$dataset->{lon});
  648. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  649. }
  650. else
  651. {
  652. Log3 ($name, 3, "$name OwnTracks: Inaccurate reading ignored: ".$dataset->{lat}.",".$dataset->{lon}." (".$dataset->{acc}.")");
  653. }
  654. #setReadingsVal($hash, "location", $dataset->{lat}.",".$dataset->{lon}, FmtDateTime($dataset->{tst}));
  655. #push(@{$hash->{CHANGED}}, "location: ".$dataset->{lat}.",".$dataset->{lon});
  656. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  657. if(defined($dataset->{alt}) and $dataset->{alt} != 0 and $accurate)
  658. {
  659. my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1);
  660. my $newaltitude = livetracking_roundfunc($dataset->{alt}/$altitudeRound)*$altitudeRound;
  661. #Log3 ($name, 0, "$name OTRound: ".$dataset->{alt}."/".$altitudeRound." = ".livetracking_roundfunc($dataset->{alt}/$altitudeRound)."*".$altitudeRound);
  662. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  663. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  664. readingsBulkUpdate($hash, "altitude", $newaltitude);
  665. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  666. #setReadingsVal($hash, "altitude", $newaltitude." m", FmtDateTime($dataset->{tst}));
  667. #push(@{$hash->{CHANGED}}, "altitude: ".$newaltitude." m");
  668. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  669. }
  670. if(defined($dataset->{tid}) and $dataset->{tid} ne "")
  671. {
  672. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  673. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  674. readingsBulkUpdate($hash, "id", $dataset->{tid});
  675. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  676. #setReadingsVal($hash, "id", $dataset->{tid}, FmtDateTime($dataset->{tst}));
  677. #push(@{$hash->{CHANGED}}, "id: "$dataset->{tid});
  678. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  679. }
  680. if(defined($dataset->{doze}) and $dataset->{doze} ne "")
  681. {
  682. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  683. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  684. readingsBulkUpdate($hash, "doze", $dataset->{doze});
  685. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  686. #setReadingsVal($hash, "doze", $dataset->{doze}, FmtDateTime($dataset->{tst}));
  687. #push(@{$hash->{CHANGED}}, "doze: "$dataset->{doze});
  688. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  689. }
  690. if(defined($dataset->{acc}) and $dataset->{acc} > 0)# and $accurate)
  691. {
  692. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  693. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  694. readingsBulkUpdate($hash, "accuracy", $dataset->{acc});
  695. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  696. #setReadingsVal($hash, "accuracy", $dataset->{acc}." m", FmtDateTime($dataset->{tst}));
  697. #push(@{$hash->{CHANGED}}, "accuracy: ".$dataset->{acc}." m");
  698. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  699. }
  700. if(defined($dataset->{vel}) and $dataset->{vel} >= 0 and $accurate)
  701. {
  702. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  703. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  704. readingsBulkUpdate($hash, "velocity", $dataset->{vel});
  705. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  706. #setReadingsVal($hash, "velocity", $dataset->{vel}." km/h", FmtDateTime($dataset->{tst}));
  707. #push(@{$hash->{CHANGED}}, "velocity: ".$dataset->{vel}." km/h");
  708. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  709. }
  710. #else
  711. #{
  712. # fhem( "deletereading $name velocity" );
  713. #}
  714. if(defined($dataset->{cog}) and $dataset->{cog} >= 0 and $accurate)
  715. {
  716. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  717. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  718. readingsBulkUpdate($hash, "heading", $dataset->{cog});
  719. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  720. #setReadingsVal($hash, "heading", $dataset->{cog}." deg", FmtDateTime($dataset->{tst}));
  721. #push(@{$hash->{CHANGED}}, "heading: ".$dataset->{cog}." deg");
  722. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  723. }
  724. #else
  725. #{
  726. # fhem( "deletereading $name heading" );
  727. #}
  728. if(defined($dataset->{batt}))
  729. {
  730. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  731. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  732. readingsBulkUpdate($hash, "batteryPercent", $dataset->{batt});
  733. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  734. readingsBulkUpdate($hash, "batteryState", (int($dataset->{batt}) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok");
  735. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  736. #setReadingsVal($hash, "battery", $dataset->{batt}." %", FmtDateTime($dataset->{tst}));
  737. #push(@{$hash->{CHANGED}}, "battery: ".$dataset->{batt}." %");
  738. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  739. }
  740. if(defined($dataset->{conn}))
  741. {
  742. readingsBulkUpdate($hash, "connection", (($dataset->{conn} eq "m")?"mobile":($dataset->{conn} eq "w")?"wifi":($dataset->{conn} eq "o")?"offline":"unknown"));
  743. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  744. }
  745. if(defined($dataset->{p}) and $dataset->{p} > 0)
  746. {
  747. readingsBulkUpdate($hash, "pressure", $dataset->{p}*10);
  748. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  749. }
  750. if(defined($dataset->{desc}) and defined($dataset->{event}))
  751. {
  752. Log3 ($name, 3, "$name OwnTracks Zone Event: ".$dataset->{event}." ".$dataset->{desc});
  753. my $place = livetracking_utf8clean($dataset->{desc});
  754. my @placenumbers;
  755. for(my $i=9;$i>=0;$i--)
  756. {
  757. next if(!defined($attr{$name}{"zonename_$i"}));
  758. push @placenumbers, $i if($place =~ m/^($attr{$name}{"zonename_$i"})$/);
  759. }
  760. if($dataset->{event} eq "enter")
  761. {
  762. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  763. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  764. readingsBulkUpdate($hash, "place", $place);
  765. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  766. #setReadingsVal($hash, "place", $dataset->{desc}, FmtDateTime($dataset->{tst}));
  767. #push(@{$hash->{CHANGED}}, "place: ".$dataset->{desc});
  768. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  769. foreach my $placenumber (@placenumbers)
  770. {
  771. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  772. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  773. readingsBulkUpdate($hash, "zone_".$placenumber,"active");
  774. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  775. #readingsSingleUpdate($hash,"zone_".$placenumber,"active",1);
  776. }
  777. }
  778. else
  779. {
  780. #fhem( "deletereading $name place" ) if(ReadingsVal($name,"place","undefined") eq $dataset->{desc});
  781. foreach my $placenumber (@placenumbers)
  782. {
  783. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  784. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  785. readingsBulkUpdate($hash, "zone_".$placenumber,"inactive");
  786. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  787. #readingsSingleUpdate($hash,"zone_".$placenumber,"inactive",1);
  788. }
  789. }
  790. }
  791. if(defined($dataset->{t}))
  792. {
  793. my $trigger = "unknown";
  794. $trigger = "ping" if($dataset->{t} eq "p");
  795. $trigger = "region" if($dataset->{t} eq "c");
  796. $trigger = "beacon" if($dataset->{t} eq "b");
  797. $trigger = "request" if($dataset->{t} eq "r");
  798. $trigger = "manual" if($dataset->{t} eq "u");
  799. $trigger = "timer" if($dataset->{t} eq "t");
  800. $trigger = "frequent" if($dataset->{t} eq "v");
  801. readingsBulkUpdate($hash, "trigger",$trigger);
  802. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  803. } else {
  804. readingsBulkUpdate($hash, "trigger","automatic");
  805. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  806. }
  807. if(defined($dataset->{inregions}))
  808. {
  809. my @placenumbersactive;
  810. my @placenumbersinactive;
  811. for(my $i=9;$i>=0;$i--)
  812. {
  813. next if(!defined($attr{$name}{"zonename_$i"}));
  814. my $active = 0;
  815. foreach my $regionname ( @{$dataset->{inregions}} )
  816. {
  817. $active = 1 if($regionname =~ m/^($attr{$name}{"zonename_$i"})$/);
  818. }
  819. if($active){
  820. Log3 ($name, 4, "$name OwnTracks region active: ".Dumper($attr{$name}{"zonename_$i"}));
  821. push @placenumbersactive, $i;
  822. } else {
  823. Log3 ($name, 4, "$name OwnTracks region inactive: ".Dumper($attr{$name}{"zonename_$i"}));
  824. push @placenumbersinactive, $i;
  825. }
  826. }
  827. foreach my $placenumber (@placenumbersactive)
  828. {
  829. readingsBulkUpdate($hash, "zone_".$placenumber,"active");
  830. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  831. }
  832. foreach my $placenumber (@placenumbersinactive)
  833. {
  834. readingsBulkUpdate($hash, "zone_".$placenumber,"inactive");
  835. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  836. }
  837. }
  838. if(defined($attr{$name}{home}) and $accurate)
  839. {
  840. #$hash->{".updateTimestamp"} = FmtDateTime($dataset->{tst});
  841. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  842. readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home}));
  843. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  844. #setReadingsVal($hash, "distance", livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km", FmtDateTime($dataset->{tst}));
  845. #push(@{$hash->{CHANGED}}, "distance: ".livetracking_distance($hash,$dataset->{lat}.",".$dataset->{lon},$attr{$name}{home})." km");
  846. #push(@{$hash->{CHANGETIME}}, FmtDateTime($dataset->{tst}));
  847. }
  848. #$hash->{CHANGETIME}[$changeindex++] = FmtDateTime($dataset->{tst});
  849. readingsEndUpdate($hash, 1);
  850. readingsSingleUpdate($hash,".lastOwnTracks",$dataset->{tst},1);
  851. $hash->{helper}{lastOwnTracks} = $dataset->{tst};
  852. return undef;
  853. }
  854. ##########################
  855. sub livetracking_dispatch($$$)
  856. {
  857. my ($param, $err, $data) = @_;
  858. my $hash = $param->{hash};
  859. my $name = $hash->{NAME};
  860. if( $err )
  861. {
  862. Log3 $name, 2, "$name: http request failed: $err";
  863. }
  864. elsif( $data )
  865. {
  866. Log3 $name, 5, "$name: $data";
  867. $data =~ s/\n//g;
  868. if( $data !~ /{.*}/ )
  869. {
  870. Log3 $name, 3, "$name: invalid json detected: >>$data<< " . $param->{type} if($data ne "[]");
  871. return undef;
  872. }
  873. my $json;
  874. $json = JSON->new->utf8(0)->decode($data);
  875. if( $param->{type} eq 'openpathsdata' ) {
  876. livetracking_ParseOpenPaths($hash,$json);
  877. } elsif( $param->{type} eq 'swarmdata' ) {
  878. livetracking_ParseSwarm($hash,$json);
  879. }
  880. }
  881. }
  882. sub livetracking_getHistory($$$$$)
  883. {
  884. my ($param,$f,$t,$srcDesc,$showData) = @_;
  885. my $hash = $param->{hash};
  886. my $name = $hash->{NAME};
  887. my (@da, $ret, @vals);
  888. my @keys = ("min","mindate","max","maxdate","currval","currdate",
  889. "firstval","firstdate","avg","cnt","lastraw");
  890. foreach my $src (@{$srcDesc->{order}}) {
  891. my $s = $srcDesc->{src}{$src};
  892. my $fname = ($src eq $defs{$name}{LOGDEVICE} ? $defs{$name}{LOGFILE} : "CURRENT");
  893. my $cmd = "get $src $fname INT $f $t ".$s->{arg};
  894. FW_fC($cmd, 1);
  895. if($showData) {
  896. $ret .= "\n$cmd\n\n";
  897. $ret .= $$internal_data if(ref $internal_data eq "SCALAR");
  898. } else {
  899. push(@da, $internal_data);
  900. for(my $i = 0; $i<=$s->{idx}; $i++) {
  901. my %h;
  902. foreach my $k (@keys) {
  903. $h{$k} = $data{$k.($i+1)};
  904. }
  905. push @vals, \%h;
  906. }
  907. }
  908. }
  909. # Reorder the $data{maxX} stuff
  910. my ($min, $max) = (999999, -999999);
  911. my $no = int(keys %{$srcDesc->{rev}});
  912. for(my $oi = 0; $oi < $no; $oi++) {
  913. my $nl = int(keys %{$srcDesc->{rev}{$oi}});
  914. for(my $li = 0; $li < $nl; $li++) {
  915. my $r = $srcDesc->{rev}{$oi}{$li}+1;
  916. my $val = shift @vals;
  917. foreach my $k (@keys) {
  918. $min = $val->{$k} if($k eq "min" && defined($val->{$k}) &&
  919. $val->{$k} =~ m/[-+]?\d*\.?\d+/ && $val->{$k} < $min);
  920. $max = $val->{$k} if($k eq "max" && defined($val->{$k}) &&
  921. $val->{$k} =~ m/[-+]?\d*\.?\d+/ && $val->{$k} > $max);
  922. $data{"$k$r"} = $val->{$k};
  923. }
  924. }
  925. }
  926. $data{maxAll} = $max;
  927. $data{minAll} = $min;
  928. return $ret if($showData);
  929. return \@da;
  930. }
  931. #########################
  932. sub livetracking_addExtension($) {
  933. my ($hash) = @_;
  934. my $name = $hash->{NAME};
  935. #livetracking_removeExtension() ;
  936. my $url = "/osmand";
  937. delete $data{FWEXT}{$url} if($data{FWEXT}{$url});
  938. Log3 $name, 1, "Enabling livetracking url for $name";
  939. $data{FWEXT}{$url}{deviceName} = $name;
  940. $data{FWEXT}{$url}{FUNC} = "livetracking_Webcall";
  941. $data{FWEXT}{$url}{LINK} = "livetracking";
  942. $modules{"livetracking"}{defptr}{"webcall".AttrVal($name, "osmandId", "")} = $hash;
  943. }
  944. #########################
  945. sub livetracking_removeExtension($) {
  946. my ($hash) = @_;
  947. my $url = "/osmand";
  948. my $name = $data{FWEXT}{$url}{deviceName};
  949. $name = $hash->{NAME} if(!defined($name));
  950. Log3 $name, 3, "Disabling livetracking url for $name";
  951. delete $data{FWEXT}{$url};
  952. delete $modules{"livetracking"}{defptr}{"webcall".AttrVal($name, "osmandId", "")};
  953. }
  954. #########################
  955. sub livetracking_Webcall() {
  956. my ($request) = @_;
  957. my ($id) = $request =~ /id=(.*?)(&|$)/ || "";
  958. my $hash = $modules{"livetracking"}{defptr}{"webcall".$id};
  959. $hash = $modules{"livetracking"}{defptr}{"webcall"} if(!defined($hash));
  960. if(!defined($hash)){
  961. Log3 "livetracking", 1, "OsmAnd webcall hash not defined!";
  962. return ( "text/plain; charset=utf-8",
  963. "undefined" );
  964. }
  965. my $name = $hash->{NAME};
  966. my $osmandid = AttrVal($name, "osmandId", undef);
  967. return undef if(defined($osmandid) && $osmandid ne $id);
  968. Log3 $name, 5, "OsmAnd webcall request:\n".$request;
  969. my ($tst) = $request =~ /tamp=(.*?)(&|$)/;
  970. my ($hdop) = $request =~ /hdop=(.*?)(&|$)/ || 0;
  971. my ($lat) = $request =~ /lat=(.*?)(&|$)/;
  972. my ($lon) = $request =~ /lon=(.*?)(&|$)/;
  973. my ($speed) = $request =~ /speed=(.*?)(&|$)/;
  974. my ($bearing) = $request =~ /bearing=(.*?)(&|$)/;
  975. my ($altitude) = $request =~ /altitude=(.*?)(&|$)/;
  976. my ($battery) = $request =~ /batt=(.*?)(&|$)/;
  977. if(!defined($tst))
  978. {
  979. return ( "text/plain; charset=utf-8",
  980. "timestamp missing" );
  981. }
  982. my $accurate = 1;
  983. $accurate = 0 if(defined($attr{$name}{filterAccuracy}) and defined($hdop) and $attr{$name}{filterAccuracy} < $hdop);
  984. my $changeindex = 0;
  985. readingsBeginUpdate($hash); # Start update readings
  986. $hash->{".updateTimestamp"} = FmtDateTime($tst);
  987. Log3 ($name, 4, "$name OsmAnd Server: ".FmtDateTime($tst));
  988. if($accurate && defined($lat) && defined($lon))
  989. {
  990. readingsBulkUpdate($hash, "location", $lat.",".$lon);
  991. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  992. }
  993. else
  994. {
  995. Log3 ($name, 3, "$name OsmAnd: Inaccurate reading ignored: ".$lat.",".$lon." (".$hdop.")");
  996. }
  997. if($accurate && defined($speed) && $speed >= 0)
  998. {
  999. readingsBulkUpdate($hash, "velocity", $speed);
  1000. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1001. }
  1002. if($accurate && defined($bearing) && $bearing >= 0)
  1003. {
  1004. readingsBulkUpdate($hash, "heading", $bearing);
  1005. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1006. }
  1007. if($accurate && defined($altitude) and $altitude != 0)
  1008. {
  1009. my $altitudeRound = AttrVal($hash->{NAME}, "roundAltitude", 1);
  1010. my $newaltitude = livetracking_roundfunc($altitude/$altitudeRound)*$altitudeRound;
  1011. readingsBulkUpdate($hash, "altitude", $newaltitude);
  1012. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1013. }
  1014. if(defined($hdop) && $hdop > 0)
  1015. {
  1016. readingsBulkUpdate($hash, "accuracy", $hdop);
  1017. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1018. }
  1019. if(defined($battery))
  1020. {
  1021. readingsBulkUpdate($hash, "batteryPercent", $battery);
  1022. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1023. readingsBulkUpdate($hash, "batteryState", (int($battery) <= int(AttrVal($name, "batteryWarning" , "20")))?"low":"ok");
  1024. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1025. }
  1026. if($accurate && defined($attr{$name}{home}) and defined($lat) && defined($lon))
  1027. {
  1028. readingsBulkUpdate($hash, "distance", livetracking_distance($hash,$lat.",".$lon,$attr{$name}{home}));
  1029. $hash->{CHANGETIME}[$changeindex++] = FmtDateTime($tst);
  1030. }
  1031. readingsEndUpdate($hash, 1);
  1032. readingsSingleUpdate($hash,".lastOsmAnd",$tst,1);
  1033. $hash->{helper}{lastOsmAnd} = $tst;
  1034. if(defined($lat) && defined($lon))
  1035. {
  1036. return ( "text/plain; charset=utf-8",
  1037. "OK" );
  1038. } else {
  1039. return ( "text/plain; charset=utf-8",
  1040. "no data" );
  1041. }
  1042. return undef;
  1043. }
  1044. ##########################
  1045. sub livetracking_DbLog_splitFn($)
  1046. {
  1047. my ($event) = @_;
  1048. my ($reading, $value, $unit) = "";
  1049. Log3 ("dbsplit", 5, "event ".$event);
  1050. my @parts = split(/ /,$event,3);
  1051. $reading = $parts[0];
  1052. $reading =~ tr/://d;
  1053. $value = $parts[1];
  1054. #Log3 ("dbsplit", 5, "split ".$parts[0]." / ".$parts[1]." / ".$parts[2]);
  1055. Log3 ("dbsplit", 5, "split ".$event);
  1056. if($event =~ m/altitude/)
  1057. {
  1058. $reading = 'altitude';
  1059. $unit = 'm';
  1060. }
  1061. elsif($event =~ m/accuracy/)
  1062. {
  1063. $reading = 'accuracy';
  1064. $unit = 'm';
  1065. }
  1066. elsif($event =~ m/distance/)
  1067. {
  1068. $reading = 'distance';
  1069. $unit = 'km';
  1070. }
  1071. elsif($event =~ m/velocity/)
  1072. {
  1073. $reading = 'velocity';
  1074. $unit = 'km/h';
  1075. }
  1076. elsif($event =~ m/heading/)
  1077. {
  1078. $reading = 'heading';
  1079. $unit = 'deg';
  1080. }
  1081. elsif($event =~ m/batteryPercent/)
  1082. {
  1083. $reading = 'batteryPercent';
  1084. $unit = '%';
  1085. }
  1086. elsif($event =~ m/batteryState/)
  1087. {
  1088. $reading = 'batteryState';
  1089. $unit = '';
  1090. }
  1091. elsif($event =~ m/steps/)
  1092. {
  1093. $reading = 'steps';
  1094. $unit = 'steps';
  1095. }
  1096. elsif($event =~ m/walking/)
  1097. {
  1098. $reading = 'walking';
  1099. $unit = 'm';
  1100. }
  1101. elsif($event =~ m/floorsup/)
  1102. {
  1103. $reading = 'floorsup';
  1104. $unit = 'floors';
  1105. }
  1106. elsif($event =~ m/floorsdown/)
  1107. {
  1108. $reading = 'floorsdown';
  1109. $unit = 'floors';
  1110. }
  1111. elsif($event =~ m/pressure/)
  1112. {
  1113. $reading = 'pressure';
  1114. $unit = 'mbar';
  1115. }
  1116. else
  1117. {
  1118. $value = $parts[1];
  1119. $value = $value." ".$parts[2] if(defined($parts[2]));
  1120. }
  1121. #Log3 ("dbsplit", 5, "output ".$reading." / ".$value." / ".$unit);
  1122. return ($reading, $value, $unit);
  1123. }
  1124. ##########################
  1125. sub livetracking_distance($$$) {
  1126. my ($hash, $loc1, $loc2) = @_;
  1127. my $name = $hash->{NAME};
  1128. my @location1 = split(',', $loc1);
  1129. my @location2 = split(',', $loc2);
  1130. my $lat1 = $location1[0];
  1131. my $lon1 = $location1[1];
  1132. my $lat2 = $location2[0];
  1133. my $lon2 = $location2[1];
  1134. my $theta = $lon1 - $lon2;
  1135. my $dist = sin(livetracking_deg2rad($lat1)) * sin(livetracking_deg2rad($lat2)) + cos(livetracking_deg2rad($lat1)) * cos(livetracking_deg2rad($lat2)) * cos(livetracking_deg2rad($theta));
  1136. $dist = livetracking_acos($dist);
  1137. $dist = livetracking_rad2deg($dist);
  1138. my $round = AttrVal($hash->{NAME}, "roundDistance", 0.1);
  1139. $dist = $dist * 60 / $round * 1.85316;
  1140. #Log3 ($name, 0, "$name DistRound: ".$dist."=".livetracking_roundfunc($dist)."*".$round);
  1141. return livetracking_roundfunc($dist)*$round;
  1142. }
  1143. sub livetracking_roundfunc($) {
  1144. my ($number) = @_;
  1145. return sprintf("%.0f", $number);
  1146. #return Math::Round::round($number);
  1147. }
  1148. sub livetracking_acos($) {
  1149. my ($rad) = @_;
  1150. my $ret = atan2(sqrt(1 - $rad**2), $rad);
  1151. return $ret;
  1152. }
  1153. sub livetracking_deg2rad($) {
  1154. my ($deg) = @_;
  1155. my $pi = atan2(1,1) * 4;
  1156. return ($deg * $pi / 180);
  1157. }
  1158. sub livetracking_rad2deg($) {
  1159. my ($rad) = @_;
  1160. my $pi = atan2(1,1) * 4;
  1161. return ($rad * 180 / $pi);
  1162. }
  1163. ##########################
  1164. sub livetracking_utf8clean($) {
  1165. my ($string) = @_;
  1166. my $log = "";
  1167. return $string if(utf8::is_utf8($string));
  1168. return encode_utf8($string);
  1169. if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/)
  1170. {
  1171. $log .= $string."(standard) ";
  1172. $string =~ s/Ä/Ae/g;
  1173. $string =~ s/Ö/Oe/g;
  1174. $string =~ s/Ü/Ue/g;
  1175. $string =~ s/ä/ae/g;
  1176. $string =~ s/ö/oe/g;
  1177. $string =~ s/ü/ue/g;
  1178. $string =~ s/ß/ss/g;
  1179. }
  1180. if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/)
  1181. {
  1182. $log .= $string."(single) ";
  1183. $string =~ s/Ä/Ae/g;
  1184. $string =~ s/Ö/Oe/g;
  1185. $string =~ s/Ü/Ue/g;
  1186. $string =~ s/ä/ae/g;
  1187. $string =~ s/ö/oe/g;
  1188. $string =~ s/ü/ue/g;
  1189. $string =~ s/ß/ss/g;
  1190. }
  1191. if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/)
  1192. {
  1193. $log .= $string."(double) ";
  1194. $string =~ s/Ä/Ae/g;
  1195. $string =~ s/Ö/Oe/g;
  1196. $string =~ s/Ü/Ue/g;
  1197. $string =~ s/ä/ae/g;
  1198. $string =~ s/ö/oe/g;
  1199. $string =~ s/ü/ue/g;
  1200. $string =~ s/ß/ss/g;
  1201. }
  1202. if($string !~ m/^[\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]+$/)
  1203. {
  1204. $log .= $string."(unknown)";
  1205. $string =~ s/[ÀÁÂÃĀĂȦẢÅǍȀȂĄẠḀẦẤẪẨẰẮẴẲǠǞǺẬẶȺⱭⱯⱰÆǼǢ]/A/g;
  1206. $string =~ s/[ḂƁḄḆƂƄɃℬ]/B/g;
  1207. $string =~ s/[ĆĈĊČƇÇḈȻ©℃]/C/g;
  1208. $string =~ s/[ḊƊḌḎḐḒĎÐĐƉƋ]/D/g;
  1209. $string =~ s/[ÈÉÊẼĒĔĖËẺĚȄȆẸȨĘḘḚỀẾỄỂḔḖỆḜƎɆƐƏ]/E/g;
  1210. $string =~ s/[ḞƑ℉]/F/g;
  1211. $string =~ s/[ǴĜḠĞĠǦƓĢǤ]/G/g;
  1212. $string =~ s/[ĤḦȞḤḨḪĦⱧⱵǶℌ]/H/g;
  1213. $string =~ s/[ÌÍÎĨĪĬİÏỈǏỊĮȈȊḬƗḮℑ]/I/g;
  1214. $string =~ s/[IJĴɈ]/J/g;
  1215. $string =~ s/[ḰǨḴƘḲĶⱩ]/K/g;
  1216. $string =~ s/[ĹḺḶĻḼĽĿŁḸȽⱠⱢ]/L/g;
  1217. $string =~ s/[ḾṀṂⱮƜℳ]/M/g;
  1218. $string =~ s/[ǸŃÑṄŇŊƝṆŅṊṈȠ№]/N/g;
  1219. $string =~ s/[ÒÓÔÕŌŎȮỎŐǑȌȎƠǪỌƟØỒỐỖỔȰȪȬṌṐṒỜỚỠỞỢǬǾƆŒƢ]/O/g;
  1220. $string =~ s/[ṔṖƤⱣ℗]/P/g;
  1221. $string =~ s/[Ɋ]/Q/g;
  1222. $string =~ s/[ŔṘŘȐȒṚŖṞṜƦɌⱤ®Ω]/R/g;
  1223. $string =~ s/[ŚŜṠŠṢȘŞⱾṤṦṨƧ℠]/S/g;
  1224. $string =~ s/[ṪŤƬƮṬȚŢṰṮŦȾ™]/T/g;
  1225. $string =~ s/[ÙÚÛŨŪŬỦŮŰǓȔȖƯỤṲŲṶṴṸṺǛǗǕǙỪỨỮỬỰɄ]/U/g;
  1226. $string =~ s/[ṼṾƲɅ]/V/g;
  1227. $string =~ s/[ẀẂŴẆẄẈⱲ]/W/g;
  1228. $string =~ s/[ẊẌ]/X/g;
  1229. $string =~ s/[ỲÝŶỸȲẎŸỶƳỴɎ]/Y/g;
  1230. $string =~ s/[ŹẐŻŽȤẒẔƵⱿⱫℨ]/Z/g;
  1231. $string =~ s/[àáâãāăȧäảåǎȁȃąạḁẚầấẫẩằắẵẳǡǟǻậặⱥɑɐɒæǽǣª]/a/g;
  1232. $string =~ s/[ḃɓḅḇƀƃƅ]/b/g;
  1233. $string =~ s/[ćĉċčƈçḉȼ]/c/g;
  1234. $string =~ s/[ḋɗḍḏḑḓďđƌȡÞþ]/d/g;
  1235. $string =~ s/[èéêẽēĕėëẻěȅȇẹȩęḙḛềếễểḕḗệḝɇɛǝⱸⱻ]/e/g;
  1236. $string =~ s/[ḟƒ]/f/g;
  1237. $string =~ s/[ǵĝḡğġǧɠģǥℊ]/g/g;
  1238. $string =~ s/[ĥḣḧȟḥḩḫẖħⱨⱶƕ]/h/g;
  1239. $string =~ s/[ìíîĩīĭıïỉǐịįȉȋḭɨḯℹ︎]/i/g;
  1240. $string =~ s/[ijĵǰȷɉ]/j/g;
  1241. $string =~ s/[ḱǩḵƙḳķĸⱪ]/k/g;
  1242. $string =~ s/[ĺḻḷļḽľŀłƚḹȴⱡ]/l/g;
  1243. $string =~ s/[ḿṁṃɱɯ]/m/g;
  1244. $string =~ s/[ǹńñṅňŋɲṇņṋṉʼnƞȵ]/n/g;
  1245. $string =~ s/[òóôõōŏȯöỏőǒȍȏơǫọɵøồốỗổȱȫȭṍṏṑṓờớỡởợǭộǿɔœƍⱷⱺƣº]/o/g;
  1246. $string =~ s/[ṕṗƥ]/p/g;
  1247. $string =~ s/[ɋ]/q/g;
  1248. $string =~ s/[ŕṙřȑȓṛŗṟṝɍⱹ]/r/g;
  1249. $string =~ s/[śŝṡšȿṥṧṩƨßſẛ]/s/g;
  1250. $string =~ s/[ṫẗťƭʈƫṭțţṱṯŧⱦȶ]/t/g;
  1251. $string =~ s/[ùúûũūŭüủůűǔȕưụṳųṷṵṹṻǜǘǖǚừứữửựʉµ]/u/g;
  1252. $string =~ s/[ṽṿⱱⱴʌ]/v/g;
  1253. $string =~ s/[ẁẃŵẇẅẘẉⱳ]/w/g;
  1254. $string =~ s/[ẋẍ]/x/g;
  1255. $string =~ s/[ỳýŷȳẏÿỷẙƴỵɏ]/y/g;
  1256. $string =~ s/[źẑżžȥẓẕƶɀⱬ]/z/g;
  1257. #$string =~ s/[^!-~\s]//g;
  1258. $string =~ s/[^\w\.,!@#$%^&*()\\|<>"' _:;\/?=+-]//g;
  1259. }
  1260. Log3 "utf8clean", 4, "Cleaned $string // $log" if($log ne "");
  1261. return $string;}
  1262. 1;
  1263. =pod
  1264. =item device
  1265. =item summary Position tracking via OwnTracks, OpenPaths and Swarm
  1266. =begin html
  1267. <a name="livetracking"></a><h3>livetracking</h3>
  1268. <ul>
  1269. This modul provides livetracking data from OpenPaths and Swarm (FourSquare).<br/>
  1270. Swarm Token: https://foursquare.com/oauth2/authenticate?client_id=EFWJ0DNVIREJ2CY1WDIFQ4MAL0ZGZAZUYCNE0NE0XZC3NCPX&response_type=token&redirect_uri=http://localhost&display=wap
  1271. <br/><br/>
  1272. <a name="livetrackingdefine"></a><b>Define</b>
  1273. <ul>
  1274. <code>define &lt;name&gt; livetracking &lt;...&gt;</code>
  1275. <br>
  1276. Example: <code>define livetrackingdata livetracking [openpaths_key] [openpaths_secret] [swarm_token]</code><br/>
  1277. Either both, just OpenPaths, just Swarm or none of them can be defined.
  1278. <br>&nbsp;
  1279. <li><code>...</code>
  1280. <br>
  1281. Reverse geocoding: Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright
  1282. </li><br>
  1283. </ul>
  1284. <br>
  1285. <a name="livetrackingget"></a><b>Get</b>
  1286. <ul>
  1287. <li><a name="#All">All</a>
  1288. <br/>
  1289. Manually trigger a data update for all sources (OpenPaths/Swarm)
  1290. </li><br>
  1291. <li><a name="#OpenPaths">OpenPaths</a>
  1292. <br/>
  1293. Manually trigger a data update for OpenPaths
  1294. </li><br>
  1295. <li><a name="#Swarm">Swarm</a>
  1296. <br/>
  1297. Manually trigger a data update for Swarm
  1298. </li><br>
  1299. <li><a name="#owntracksLocation">owntracksLocation</a>
  1300. <br/>
  1301. Request position from OwnTracks
  1302. </li><br>
  1303. <li><a name="#owntracksSteps">owntracksSteps</a>
  1304. <br/>
  1305. Request steps data from OwnTracks
  1306. </li><br>
  1307. <li><a name="#address">address [&lt;lat,lng&gt;/short/long]</a>
  1308. <br/>
  1309. Get an address from coordinates<br/>
  1310. </li><br>
  1311. </ul>
  1312. <br>
  1313. <a name="livetrackingset"></a><b>Set</b>
  1314. <ul>
  1315. <li><a name="#owntracksMessage">owntracksMessage</a>
  1316. <br>
  1317. Send a message to OwnTracks
  1318. </li><br>
  1319. </ul>
  1320. <br>
  1321. <a name="livetrackingreadings"></a><b>Readings</b>
  1322. <ul>
  1323. <li><code>location</code>
  1324. <br>
  1325. GPS position
  1326. </li><br>
  1327. <li><code>distance</code> (km)
  1328. <br>
  1329. GPS distance from home
  1330. </li><br>
  1331. <li><code>accuracy</code> (m)
  1332. <br>
  1333. GPS accuracy
  1334. </li><br>
  1335. <li><code>altitude</code> (m)
  1336. <br>
  1337. GPS altitude
  1338. </li><br>
  1339. <li><code>velocity</code> (km/h)
  1340. <br>
  1341. GPS velocity
  1342. </li><br>
  1343. <li><code>heading</code> (deg)
  1344. <br>
  1345. GPS heading
  1346. </li><br>
  1347. <li><code>place</code>
  1348. <br>
  1349. Swarm place name
  1350. </li><br>
  1351. <li><code>steps</code> (steps)
  1352. <br>
  1353. iOS walked steps
  1354. </li><br>
  1355. <li><code>walking</code> (m)
  1356. <br>
  1357. iOS walked distance
  1358. </li><br>
  1359. <li><code>floorsup</code> (floors)
  1360. <br>
  1361. iOS floors walked up
  1362. </li><br>
  1363. <li><code>floorsdown</code> (floors)
  1364. <br>
  1365. iOS floors walked down
  1366. </li><br>
  1367. <li><code>zone_N</code> (active/inactive)
  1368. <br>
  1369. Zone status in OwnTracks
  1370. </li><br>
  1371. <li><code>beacon</code>
  1372. <br>
  1373. Beacon ID from OwnTracks
  1374. </li><br>
  1375. <li><code>beacon_N_X</code>
  1376. <br>
  1377. Beacon data for saved beacons for indoor positioning
  1378. </li><br>
  1379. <li><code>batteryState</code> (ok/low)
  1380. <br>
  1381. Battery state (can be set through attribute batteryWarning )
  1382. </li><br>
  1383. <li><code>batteryPercent</code> (%)
  1384. <br>
  1385. Battery percentage
  1386. </li><br>
  1387. <li><code>connection</code> (mobile/wifi/offline/unknown)
  1388. <br>
  1389. Phone connection type from OwnTracks at last position
  1390. </li><br>
  1391. </ul>
  1392. <br>
  1393. <a name="livetrackingattr"></a><b>Attributes</b>
  1394. <ul>
  1395. <li><a name="batteryWarning">batteryWarning</a> (%)
  1396. <br>
  1397. Set battery ok/low threshold
  1398. </li><br>
  1399. <li><a name="beacon_0">beacon_N</a>
  1400. <br>
  1401. Saved beacon IDs from OwnTracks for indoor positioning, e.g.:<br/>
  1402. FDA50693-A4E2-4FB1-AFCF-C6EB07647825,19789,1
  1403. </li><br>
  1404. <li><a name="zonename_0">zonename_N</a>
  1405. <br>
  1406. Assign zone name from OwnTracks
  1407. </li><br>
  1408. <li><a name="home">home</a> (lat,lon)
  1409. <br>
  1410. Home location
  1411. </li><br>
  1412. <li><a name="swarmHome">swarmHome</a> (lat,lon)
  1413. <br>
  1414. Fake home location (that is assigned to private homes for security reasons) of your Swarm home (exact position)
  1415. </li><br>
  1416. <li><a name="filterAccuracy">filterAccuracy</a> (m)
  1417. <br>
  1418. Minimum accuracy of GPS location to update any readings
  1419. </li><br>
  1420. <li><a name="roundDistance">roundDistance</a> (km)
  1421. <br>
  1422. Rounding for distance reading to prevent too many changes
  1423. </li><br>
  1424. <li><a name="roundAltitude">roundAltitude</a> (m)
  1425. <br>
  1426. Rounding for altitude reading to prevent too many changes
  1427. </li><br>
  1428. <li><a name="owntracksDevice">owntracksDevice</a>
  1429. <br>
  1430. OwnTracks MQTT device to look for notifies from
  1431. </li><br>
  1432. <li><a name="addressLanguage">addressLanguage</a> (de/en/fr/es/it/nl)
  1433. <br>
  1434. Preferred language used to return reverse geocoding results
  1435. </li><br>
  1436. <li><a name="addressReading">createAddressReading</a> (0/1)
  1437. <br>
  1438. Write reverse geocoding results to address reading
  1439. </li><br>
  1440. <li><a name="osmandServer">osmandServer</a> (0/1)
  1441. <br>
  1442. Starts an OsmAnd compatible listener on FHEM which can be entered into traccar-client directly:<br/>
  1443. <code>https://user:pass@your.fhem.ip/fhem/osmand</code> (The Android client does not support user:pass authentication)
  1444. </li><br>
  1445. <li><a name="osmandId">osmandId</a>
  1446. <br>
  1447. The device identifier that is set in the OsmAnd client and transmitted in the request as <i>id</i>
  1448. </li><br>
  1449. </ul>
  1450. </ul>
  1451. =end html
  1452. =cut