98_GEOFANCY.pm 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. ###############################################################################
  2. # $Id: 98_GEOFANCY.pm 17593 2018-10-22 15:35:04Z loredo $
  3. package main;
  4. use strict;
  5. use warnings;
  6. use Data::Dumper;
  7. use Time::Local;
  8. use UConv;
  9. use HttpUtils;
  10. # initialize ##################################################################
  11. sub GEOFANCY_Initialize($) {
  12. my ($hash) = @_;
  13. $hash->{DefFn} = "GEOFANCY_Define";
  14. $hash->{UndefFn} = "GEOFANCY_Undefine";
  15. $hash->{SetFn} = "GEOFANCY_Set";
  16. $hash->{AttrList} = "devAlias disable:0,1 " . $readingFnAttributes;
  17. }
  18. # regular Fn ##################################################################
  19. sub GEOFANCY_Define($$) {
  20. my ( $hash, $def ) = @_;
  21. my @a = split( "[ \t]+", $def, 5 );
  22. return "Usage: define <name> GEOFANCY <infix>"
  23. if ( int(@a) != 3 );
  24. my $name = $a[0];
  25. my $infix = $a[2];
  26. $hash->{fhem}{infix} = $infix;
  27. GEOFANCY_addExtension( $name, "GEOFANCY_CGI", $infix );
  28. readingsBeginUpdate($hash);
  29. readingsBulkUpdate( $hash, "state", "initialized" );
  30. readingsEndUpdate( $hash, 1 );
  31. return undef;
  32. }
  33. sub GEOFANCY_Undefine($$) {
  34. my ( $hash, $name ) = @_;
  35. GEOFANCY_removeExtension( $hash->{fhem}{infix} );
  36. return undef;
  37. }
  38. sub GEOFANCY_Set($@) {
  39. my ( $hash, @a ) = @_;
  40. my $name = $hash->{NAME};
  41. my $state = $hash->{STATE};
  42. Log3 $name, 5, "GEOFANCY $name: called function GEOFANCY_Set()";
  43. return "No Argument given" if ( !defined( $a[1] ) );
  44. my $usage = "Unknown argument " . $a[1] . ", choose one of clear:readings";
  45. # clear
  46. if ( $a[1] eq "clear" ) {
  47. Log3 $name, 2, "GEOFANCY set $name " . $a[1];
  48. if ( $a[2] ) {
  49. # readings
  50. if ( $a[2] eq "readings" ) {
  51. delete $hash->{READINGS};
  52. readingsBeginUpdate($hash);
  53. readingsBulkUpdate( $hash, "state", "clearedReadings" );
  54. readingsEndUpdate( $hash, 1 );
  55. }
  56. }
  57. else {
  58. return "No Argument given, choose one of readings ";
  59. }
  60. }
  61. # return usage hint
  62. else {
  63. return $usage;
  64. }
  65. return undef;
  66. }
  67. # module Fn ####################################################################
  68. sub GEOFANCY_CGI() {
  69. # Locative.app (https://itunes.apple.com/us/app/locative/id725198453?mt=8)
  70. # /$infix?device=UUIDdev&id=UUIDloc&latitude=xx.x&longitude=xx.x&trigger=(enter|exit)
  71. #
  72. # Geofency.app (https://itunes.apple.com/us/app/geofency-time-tracking-automatic/id615538630?mt=8)
  73. # /$infix?id=UUIDloc&name=locName&entry=(1|0)&date=DATE&latitude=xx.x&longitude=xx.x&device=UUIDdev
  74. #
  75. # SMART Geofences.app (https://www.microsoft.com/en-us/store/apps/smart-geofences/9nblggh4rk3k)
  76. # /$infix?device=UUIDdev&name=UUIDloc&latitude=xx.x&longitude=xx.x&type=(Entered|Leaving)&date=DATE
  77. #
  78. my ($request) = @_;
  79. my $hash;
  80. my $name = "";
  81. my $link = "";
  82. my $URI = "";
  83. my $device = "";
  84. my $deviceAlias = "-";
  85. my $id = "";
  86. my $lat = "";
  87. my $long = "";
  88. my $posLat = "";
  89. my $posLong = "";
  90. my $posBeaconUUID = "";
  91. my $posTravDist = "";
  92. my $address = "-";
  93. my $posAddress = "-";
  94. my $entry = "";
  95. my $msg = "";
  96. my $date = "";
  97. my $time = "";
  98. my $locName = "";
  99. my $radius = 0;
  100. my $posDistLoc = "";
  101. my $posDistHome = "";
  102. my $locTravDist = "";
  103. my $motion = "";
  104. my $wifiSSID = "";
  105. my $wifiBSSID = "";
  106. # data received
  107. if ( $request =~ m,^(\/[^/]+?)(?:\&|\?|\/\?|\/)(.*)?$, ) {
  108. $link = $1;
  109. $URI = $2;
  110. # get device name
  111. $name = $data{FWEXT}{$link}{deviceName} if ( $data{FWEXT}{$link} );
  112. # return error if no such device
  113. return ( "text/plain; charset=utf-8",
  114. "NOK No GEOFANCY device for webhook $link" )
  115. unless ($name);
  116. # return error if no such device
  117. return ( "text/plain; charset=utf-8", "NOK disabled" )
  118. if ( IsDisabled($name) );
  119. # extract values from URI
  120. my $webArgs;
  121. foreach my $pv ( split( "&", $URI ) ) {
  122. next if ( $pv eq "" );
  123. $pv =~ s/\+/ /g;
  124. $pv =~ s/%([\dA-F][\dA-F])/chr(hex($1))/ige;
  125. my ( $p, $v ) = split( "=", $pv, 2 );
  126. $webArgs->{$p} = trim($v);
  127. }
  128. # validate id
  129. # does not exist in "SMART Geofences.app"
  130. return ( "text/plain; charset=utf-8",
  131. "NOK Expected value for 'id' cannot be empty" )
  132. if ( ( !defined( $webArgs->{id} ) || $webArgs->{id} eq "" )
  133. && !defined( $webArgs->{type} ) );
  134. return ( "text/plain; charset=utf-8",
  135. "NOK No whitespace allowed in id '" . $webArgs->{id} . "'" )
  136. if ( defined( $webArgs->{id} ) && $webArgs->{id} =~ m/(?:\s)/ );
  137. # validate locName
  138. return ( "text/plain; charset=utf-8",
  139. "NOK No whitespace allowed in id '" . $webArgs->{locName} . "'" )
  140. if ( defined( $webArgs->{locName} )
  141. && $webArgs->{locName} =~ m/(?:\s)/ );
  142. # require entry or trigger
  143. return ( "text/plain; charset=utf-8",
  144. "NOK Neither 'entry' nor 'trigger' nor 'type' was specified" )
  145. if ( !defined( $webArgs->{entry} )
  146. && !defined( $webArgs->{trigger} )
  147. && !defined( $webArgs->{type} ) );
  148. # validate entry
  149. return ( "text/plain; charset=utf-8",
  150. "NOK Expected value for 'entry' cannot be empty" )
  151. if ( defined( $webArgs->{entry} ) && $webArgs->{entry} eq "" );
  152. return ( "text/plain; charset=utf-8",
  153. "NOK Value for 'entry' can only be: 1 0" )
  154. if ( defined( $webArgs->{entry} )
  155. && $webArgs->{entry} ne 0
  156. && $webArgs->{entry} ne 1 );
  157. # validate trigger
  158. return ( "text/plain; charset=utf-8",
  159. "NOK Expected value for 'trigger' cannot be empty" )
  160. if ( defined( $webArgs->{trigger} ) && $webArgs->{trigger} eq "" );
  161. return ( "text/plain; charset=utf-8",
  162. "NOK Value for 'trigger' can only be: enter|test exit" )
  163. if ( defined( $webArgs->{trigger} )
  164. && $webArgs->{trigger} ne "enter"
  165. && $webArgs->{trigger} ne "test"
  166. && $webArgs->{trigger} ne "exit" );
  167. # validate type
  168. return ( "text/plain; charset=utf-8",
  169. "NOK Expected value for 'type' cannot be empty" )
  170. if ( defined( $webArgs->{type} ) && $webArgs->{type} eq "" );
  171. return ( "text/plain; charset=utf-8",
  172. "NOK Value for 'type' can only be: Entered Leaving" )
  173. if ( defined( $webArgs->{type} )
  174. && lc( $webArgs->{type} ) ne "entered"
  175. && lc( $webArgs->{type} ) ne "leaving" );
  176. # validate date
  177. return (
  178. "text/plain; charset=utf-8",
  179. "NOK Specified date '"
  180. . $webArgs->{date} . "'"
  181. . " does not match ISO8601 UTC format (1970-01-01T00:00:00Z)"
  182. )
  183. if ( defined( $webArgs->{date} )
  184. && $webArgs->{date} !~
  185. m/(19|20)\d\d-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]\.?[0-9]*)Z/
  186. );
  187. # validate timestamp
  188. return (
  189. "text/plain; charset=utf-8",
  190. "NOK Specified timestamp '"
  191. . $webArgs->{timestamp} . "'"
  192. . " does not seem to be a valid Unix timestamp"
  193. )
  194. if (
  195. defined( $webArgs->{timestamp} )
  196. && ( $webArgs->{timestamp} !~ m/^\d+(\.\d+)?$/
  197. || $webArgs->{timestamp} > time() + 300 )
  198. );
  199. # validate locName
  200. return ( "text/plain; charset=utf-8",
  201. "NOK No whitespace allowed in id '" . $webArgs->{locName} . "'" )
  202. if ( defined( $webArgs->{locName} )
  203. && $webArgs->{locName} =~ m/(?:\s)/ );
  204. # validate LAT
  205. return (
  206. "text/plain; charset=utf-8",
  207. "NOK Specified latitude '"
  208. . $webArgs->{latitude}
  209. . "' has unexpected format"
  210. )
  211. if (
  212. defined $webArgs->{latitude}
  213. && ( $webArgs->{latitude} !~ m/^-?\d+(\.\d+)?$/
  214. || $webArgs->{latitude} < -90
  215. || $webArgs->{latitude} > 90 )
  216. );
  217. # validate LONG
  218. return (
  219. "text/plain; charset=utf-8",
  220. "NOK Specified longitude '"
  221. . $webArgs->{longitude}
  222. . "' has unexpected format"
  223. )
  224. if (
  225. defined $webArgs->{longitude}
  226. && ( $webArgs->{longitude} !~ m/^-?\d+(\.\d+)?$/
  227. || $webArgs->{longitude} < -180
  228. || $webArgs->{longitude} > 180 )
  229. );
  230. # validate posLAT
  231. return (
  232. "text/plain; charset=utf-8",
  233. "NOK Specified latitude '"
  234. . $webArgs->{currentLatitude}
  235. . "' has unexpected format"
  236. )
  237. if (
  238. defined $webArgs->{currentLatitude}
  239. && ( $webArgs->{currentLatitude} !~ m/^-?\d+(\.\d+)?$/
  240. || $webArgs->{currentLatitude} < -90
  241. || $webArgs->{currentLatitude} > 90 )
  242. );
  243. # validate posLONG
  244. return (
  245. "text/plain; charset=utf-8",
  246. "NOK Specified longitude '"
  247. . $webArgs->{currentLongitude}
  248. . "' has unexpected format"
  249. )
  250. if (
  251. defined $webArgs->{currentLongitude}
  252. && ( $webArgs->{currentLongitude} !~ m/^-?\d+(\.\d+)?$/
  253. || $webArgs->{currentLongitude} < -180
  254. || $webArgs->{currentLongitude} > 180 )
  255. );
  256. # validate device
  257. return ( "text/plain; charset=utf-8",
  258. "NOK Expected value for 'device' cannot be empty" )
  259. if ( !defined( $webArgs->{device} ) || $webArgs->{device} eq "" );
  260. return (
  261. "text/plain; charset=utf-8",
  262. "NOK No whitespace allowed in device '" . $webArgs->{device} . "'"
  263. )
  264. if ( defined( $webArgs->{device} )
  265. && $webArgs->{device} =~ m/(?:\s)/ );
  266. # validate motion
  267. if ( defined( $webArgs->{motion} ) ) {
  268. my @motions = (
  269. "unknown", "stationary", "walking", "running",
  270. "automotive", "cycling"
  271. );
  272. my $motionLc = lc( $webArgs->{motion} );
  273. return ( "text/plain; charset=utf-8",
  274. "NOK Unknown motion type '" . $webArgs->{motion} . "'" )
  275. if ( !grep( /^$motionLc$/, @motions ) );
  276. }
  277. # Locative.app
  278. if ( defined $webArgs->{trigger} ) {
  279. Log3 $name, 5, "GEOFANCY $name: detected data format: Locative.app";
  280. $id = $webArgs->{id};
  281. $entry = $webArgs->{trigger};
  282. $lat = $webArgs->{latitude};
  283. $long = $webArgs->{longitude};
  284. $device = $webArgs->{device};
  285. $posLat = $lat;
  286. $posLong = $long;
  287. if ( defined( $webArgs->{timestamp} ) ) {
  288. my ( $sec, $min, $hour, $d, $m, $y ) =
  289. localtime( $webArgs->{timestamp} );
  290. $date = timelocal( $sec, $min, $hour, $d, $m, $y );
  291. }
  292. }
  293. # Geofency.app
  294. elsif ( defined $webArgs->{entry} ) {
  295. Log3 $name, 5, "GEOFANCY $name: detected data format: Geofency.app";
  296. $id = $webArgs->{id};
  297. $locName = $webArgs->{name};
  298. $entry = $webArgs->{entry};
  299. $date = GEOFANCY_ISO8601UTCtoLocal( $webArgs->{date} );
  300. $lat = $webArgs->{latitude};
  301. $long = $webArgs->{longitude};
  302. $radius = $webArgs->{radius}
  303. if ( defined( $webArgs->{radius} ) );
  304. $address = $webArgs->{address}
  305. if ( defined( $webArgs->{address} ) );
  306. $device = $webArgs->{device};
  307. $motion = $webArgs->{motion}
  308. if ( defined( $webArgs->{motion} ) );
  309. $wifiSSID = $webArgs->{wifiSSID}
  310. if ( defined( $webArgs->{wifiSSID} ) );
  311. $wifiBSSID = $webArgs->{wifiBSSID}
  312. if ( defined( $webArgs->{wifiBSSID} ) );
  313. $posBeaconUUID = $webArgs->{beaconUUID}
  314. if ( defined( $webArgs->{beaconUUID} ) );
  315. if (
  316. !defined( $webArgs->{currentLatitude} )
  317. || !defined( $webArgs->{currentLongitude} )
  318. || ( $webArgs->{currentLatitude} == 0
  319. && $webArgs->{currentLongitude} == 0 )
  320. )
  321. {
  322. $posLat = $webArgs->{latitude};
  323. $posLong = $webArgs->{longitude};
  324. $posAddress = $address;
  325. }
  326. else {
  327. $posLat = $webArgs->{currentLatitude};
  328. $posLong = $webArgs->{currentLongitude};
  329. $posAddress = $address if ( $posBeaconUUID ne "" );
  330. }
  331. }
  332. # SMART Geofences.app
  333. elsif ( defined $webArgs->{type} ) {
  334. Log3 $name, 5,
  335. "GEOFANCY $name: detected data format: SMART Geofences.app";
  336. $id = $webArgs->{name};
  337. $locName = $webArgs->{name};
  338. $entry = $webArgs->{type};
  339. $date = GEOFANCY_ISO8601UTCtoLocal( $webArgs->{date} );
  340. $lat = $webArgs->{latitude};
  341. $long = $webArgs->{longitude};
  342. $address = $webArgs->{address}
  343. if ( defined( $webArgs->{address} ) );
  344. $device = $webArgs->{device};
  345. $posLat = $lat;
  346. $posLong = $long;
  347. $posAddress = $address;
  348. }
  349. else {
  350. return "fatal error";
  351. }
  352. }
  353. # no data received
  354. else {
  355. Log3 undef, 5, "GEOFANCY: No data received";
  356. return ( "text/plain; charset=utf-8", "NOK No data received" );
  357. }
  358. # return error if unknown trigger
  359. return ( "text/plain; charset=utf-8", "$entry NOK" )
  360. if ( lc($entry) ne "enter"
  361. && lc($entry) ne "1"
  362. && lc($entry) ne "exit"
  363. && lc($entry) ne "0"
  364. && lc($entry) ne "test"
  365. && lc($entry) ne "entered"
  366. && lc($entry) ne "leaving" );
  367. $hash = $defs{$name};
  368. # update ROOMMATE devices associated with this device UUID
  369. my $matchingResident = 0;
  370. delete $hash->{ROOMMATES};
  371. foreach my $gdev ( devspec2array("rr_geofenceUUIDs=.+") ) {
  372. next unless ( IsDevice( $gdev, "ROOMMATE" ) );
  373. Log3 $name, 5, "GEOFANCY $name: Checking rr_geofenceUUIDs for $gdev";
  374. my $geofenceUUIDs = AttrVal( $gdev, "rr_geofenceUUIDs", undef );
  375. $hash->{ROOMMATES} .= ",$gdev" if $hash->{ROOMMATES};
  376. $hash->{ROOMMATES} = $gdev if !$hash->{ROOMMATES};
  377. my @UUIDs = split( ',', $geofenceUUIDs );
  378. if (@UUIDs) {
  379. foreach (@UUIDs) {
  380. if ( $_ eq $device ) {
  381. Log3 $name, 4,
  382. "GEOFANCY $name: "
  383. . "Found matching UUID at ROOMMATE device $gdev";
  384. $deviceAlias = $gdev;
  385. $matchingResident = 1;
  386. last;
  387. }
  388. }
  389. }
  390. }
  391. # update GUEST devices associated with this device UUID
  392. delete $hash->{GUESTS};
  393. foreach my $gdev ( devspec2array("rg_geofenceUUIDs=.+") ) {
  394. next unless ( IsDevice( $gdev, "GUEST" ) );
  395. Log3 $name, 5, "GEOFANCY $name: Checking rg_geofenceUUIDs for $gdev";
  396. my $geofenceUUIDs = AttrVal( $gdev, "rg_geofenceUUIDs", undef );
  397. $hash->{GUESTS} .= ",$gdev" if $hash->{GUESTS};
  398. $hash->{GUESTS} = $gdev if !$hash->{GUESTS};
  399. my @UUIDs = split( ',', $geofenceUUIDs );
  400. if (@UUIDs) {
  401. foreach (@UUIDs) {
  402. if ( $_ eq $device ) {
  403. Log3 $name, 4,
  404. "GEOFANCY $name: "
  405. . "Found matching UUID at GUESTS device $gdev";
  406. $deviceAlias = $gdev;
  407. $matchingResident = 1;
  408. last;
  409. }
  410. }
  411. }
  412. }
  413. # Device alias handling
  414. #
  415. delete $hash->{helper}{device_aliases}
  416. if $hash->{helper}{device_aliases};
  417. delete $hash->{helper}{device_names}
  418. if $hash->{helper}{device_names};
  419. my @devices = split( ' ', AttrVal( $name, "devAlias", "" ) );
  420. foreach (@devices) {
  421. my @device = split( ':', $_ );
  422. $hash->{helper}{device_aliases}{ $device[0] } =
  423. $device[1];
  424. $hash->{helper}{device_names}{ $device[1] } =
  425. $device[0];
  426. }
  427. $deviceAlias = $hash->{helper}{device_aliases}{$device}
  428. if ( $hash->{helper}{device_aliases}{$device}
  429. && $matchingResident == 0 );
  430. Log3 $name, 4,
  431. "GEOFANCY $name: id=$id name=$locName trig=$entry date=$date lat=$lat long=$long posLat=$posLat posLong=$posLong address:$address dev=$device devAlias=$deviceAlias motion=$motion wifiSSID=$wifiSSID wifiBSSID=$wifiBSSID";
  432. Log3 $name, 3,
  433. "GEOFANCY $name: Unknown device UUID $device: Set attribute devAlias for $name or assign $device to any ROOMMATE or GUEST device using attribute r*_geofenceUUIDs"
  434. if ( $deviceAlias eq "-" );
  435. # distance between home and position
  436. my $homeLat = AttrVal( "global", "latitude", undef );
  437. my $homeLong = AttrVal( "global", "longitude", undef );
  438. if ( $homeLat && $homeLong ) {
  439. if ( $posLat ne "" && $posLong ne "" ) {
  440. $posDistHome =
  441. UConv::distance( $posLat, $posLong, $homeLat, $homeLong, 2 );
  442. }
  443. elsif ( $lat ne "" && $long ne "" ) {
  444. $posDistHome =
  445. UConv::distance( $lat, $long, $homeLat, $homeLong, 2 );
  446. }
  447. }
  448. # distance between location and position
  449. if ( $lat ne "" && $long ne "" && $posLat ne "" && $posLong ne "" ) {
  450. $posDistLoc = UConv::distance( $posLat, $posLong, $lat, $long, 2 );
  451. }
  452. # travelled distance for location
  453. if ( $lat ne "" && $long ne "" ) {
  454. my $locLatVal = ReadingsVal( $name, "currLocLat_" . $deviceAlias, "-" );
  455. $locLatVal = ReadingsVal( $name, "lastLocLat_" . $deviceAlias, "-" )
  456. if ( $locLatVal eq "-" );
  457. my $locLongVal =
  458. ReadingsVal( $name, "currLocLong_" . $deviceAlias, "-" );
  459. $locLongVal = ReadingsVal( $name, "lastLocLong_" . $deviceAlias, "-" )
  460. if ( $locLongVal eq "-" );
  461. if ( $locLatVal ne "-" && $locLongVal ne "-" ) {
  462. $locTravDist =
  463. UConv::distance( $lat, $long, $locLatVal, $locLongVal, 2 );
  464. }
  465. }
  466. # travelled distance for position
  467. if ( $posLat ne "" && $posLong ne "" ) {
  468. my $currPosLatVal =
  469. ReadingsVal( $name, "currPosLat_" . $deviceAlias, "" );
  470. my $currPosLongVal =
  471. ReadingsVal( $name, "currPosLong_" . $deviceAlias, "" );
  472. if ( $currPosLatVal ne "" && $currPosLongVal ne "" ) {
  473. $posTravDist = UConv::distance( $posLat, $posLong, $currPosLatVal,
  474. $currPosLongVal, 2 );
  475. }
  476. }
  477. readingsBeginUpdate($hash);
  478. # use date for readings
  479. if ( $date ne "" ) {
  480. $hash->{".updateTime"} = $date;
  481. $hash->{".updateTimestamp"} = FmtDateTime( $hash->{".updateTime"} );
  482. $time = $hash->{".updateTimestamp"};
  483. }
  484. # use local FHEM time
  485. else {
  486. $time = TimeNow();
  487. }
  488. # General readings
  489. readingsBulkUpdate( $hash, "state",
  490. "id:$id trig:$entry date:$date lat:$lat long:$long dev:$device devAlias=$deviceAlias"
  491. );
  492. readingsBulkUpdate( $hash, "lastDeviceUUID", $device );
  493. readingsBulkUpdate( $hash, "lastDevice", $deviceAlias );
  494. if ( $deviceAlias ne "-" ) {
  495. $id = $locName if ( defined($locName) && $locName ne "" );
  496. readingsBulkUpdate( $hash, "lastArr", $deviceAlias . " " . $id )
  497. if ( lc($entry) eq "enter"
  498. || lc($entry) eq "1"
  499. || lc($entry) eq "entered" );
  500. readingsBulkUpdate( $hash, "lastDep", $deviceAlias . " " . $id )
  501. if ( lc($entry) eq "exit"
  502. || lc($entry) eq "0"
  503. || lc($entry) eq "leaving" );
  504. my $currReading;
  505. my $lastReading;
  506. my $currVal;
  507. # backup last known position
  508. $currReading = "currPosTime_" . $deviceAlias;
  509. $currVal = ReadingsVal( $name, $currReading, undef );
  510. readingsBulkUpdate( $hash, "lastPosArr_" . $deviceAlias, $currVal );
  511. if ($currVal) {
  512. readingsBulkUpdate( $hash, "lastPosDep_" . $deviceAlias, $time );
  513. readingsBulkUpdate(
  514. $hash,
  515. "lastPosDur_" . $deviceAlias,
  516. UConv::duration( $time, $currVal, "sec" )
  517. );
  518. }
  519. foreach (
  520. 'PosSSID', 'PosBSSID', 'PosMotion', 'PosLat',
  521. 'PosLong', 'PosAddr', 'PosBeaconUUID', 'PosDistHome',
  522. 'PosDistLoc', 'PosTravDist', 'LocTravDist', 'LocRadius'
  523. )
  524. {
  525. $currReading = "curr" . $_ . "_" . $deviceAlias;
  526. $lastReading = "last" . $_ . "_" . $deviceAlias;
  527. $currVal = ReadingsVal( $name, $currReading, undef );
  528. readingsBulkUpdate( $hash, $lastReading, $currVal )
  529. if ( defined($currVal) );
  530. }
  531. readingsBulkUpdate( $hash, "currPosSSID_" . $deviceAlias, $wifiSSID );
  532. readingsBulkUpdate( $hash, "currPosBSSID_" . $deviceAlias, $wifiBSSID );
  533. readingsBulkUpdate( $hash, "currPosMotion_" . $deviceAlias, $motion );
  534. readingsBulkUpdate( $hash, "currPosBeaconUUID_" . $deviceAlias,
  535. $posBeaconUUID );
  536. readingsBulkUpdate( $hash, "currPosLat_" . $deviceAlias, $posLat );
  537. readingsBulkUpdate( $hash, "currPosLong_" . $deviceAlias, $posLong );
  538. readingsBulkUpdate( $hash, "currPosAddr_" . $deviceAlias, $posAddress );
  539. readingsBulkUpdate( $hash, "currPosTime_" . $deviceAlias, $time );
  540. readingsBulkUpdate( $hash, "currPosDistHome_" . $deviceAlias,
  541. $posDistHome );
  542. readingsBulkUpdate( $hash, "currPosDistLoc_" . $deviceAlias,
  543. $posDistLoc );
  544. readingsBulkUpdate( $hash, "currLocTravDist_" . $deviceAlias,
  545. $locTravDist );
  546. readingsBulkUpdate( $hash, "currPosTravDist_" . $deviceAlias,
  547. $posTravDist );
  548. readingsBulkUpdate( $hash, "currLocRadius_" . $deviceAlias, $radius );
  549. if ( lc($entry) eq "enter"
  550. || lc($entry) eq "1"
  551. || lc($entry) eq "entered"
  552. || lc($entry) eq "test" )
  553. {
  554. Log3 $name, 4, "GEOFANCY $name: $deviceAlias arrived at $id";
  555. readingsBulkUpdate( $hash, $deviceAlias, "arrived " . $id );
  556. readingsBulkUpdate( $hash, "currLoc_" . $deviceAlias, $id );
  557. readingsBulkUpdate( $hash, "currLocLat_" . $deviceAlias, $lat );
  558. readingsBulkUpdate( $hash, "currLocLong_" . $deviceAlias, $long );
  559. readingsBulkUpdate( $hash, "currLocAddr_" . $deviceAlias,
  560. $address );
  561. readingsBulkUpdate( $hash, "currLocTime_" . $deviceAlias, $time );
  562. }
  563. elsif (lc($entry) eq "exit"
  564. || lc($entry) eq "0"
  565. || lc($entry) eq "leaving" )
  566. {
  567. Log3 $name, 4,
  568. "GEOFANCY $name: $deviceAlias left $id and is in transit";
  569. # backup last known location if not "underway"
  570. $currReading = "currLocTime_" . $deviceAlias;
  571. $currVal = ReadingsVal( $name, $currReading, undef );
  572. if ( $currVal && $currVal ne "underway" ) {
  573. readingsBulkUpdate( $hash, "lastLocArr_" . $deviceAlias,
  574. $currVal );
  575. readingsBulkUpdate( $hash, "lastLocDep_" . $deviceAlias,
  576. $time );
  577. readingsBulkUpdate(
  578. $hash,
  579. "lastLocDur_" . $deviceAlias,
  580. UConv::duration( $time, $currVal, "sec" )
  581. );
  582. foreach ( 'Loc', 'LocLat', 'LocLong', 'LocAddr' ) {
  583. $currReading = "curr" . $_ . "_" . $deviceAlias;
  584. $lastReading = "last" . $_ . "_" . $deviceAlias;
  585. $currVal = ReadingsVal( $name, $currReading, "" );
  586. $currVal = ReadingsVal( $name, $lastReading, "" )
  587. if ( $currVal eq "-" || $currVal eq "" );
  588. readingsBulkUpdate( $hash, $lastReading, $currVal );
  589. }
  590. }
  591. readingsBulkUpdate( $hash, $deviceAlias, "left " . $id );
  592. readingsBulkUpdate( $hash, "currLoc_" . $deviceAlias, "underway" );
  593. readingsBulkUpdate( $hash, "currLocLat_" . $deviceAlias, "-" );
  594. readingsBulkUpdate( $hash, "currLocLong_" . $deviceAlias, "-" );
  595. readingsBulkUpdate( $hash, "currLocAddr_" . $deviceAlias, "-" );
  596. readingsBulkUpdate( $hash, "currLocTime_" . $deviceAlias, $time );
  597. }
  598. }
  599. readingsEndUpdate( $hash, 1 );
  600. # trigger update of resident device readings
  601. if ( $matchingResident == 1 ) {
  602. my $trigger = 0;
  603. $trigger = 1
  604. if ( lc($entry) eq "enter"
  605. || lc($entry) eq "1"
  606. || lc($entry) eq "entered"
  607. || lc($entry) eq "test" );
  608. $locName = $id if ( $locName eq "" );
  609. RESIDENTStk_SetLocation(
  610. $deviceAlias, $locName, $trigger, $id,
  611. $time, $lat, $long, $address,
  612. $device, $radius, $posLat, $posLong,
  613. $posAddress, $posBeaconUUID, $posDistHome, $posDistLoc,
  614. $motion, $wifiSSID, $wifiBSSID
  615. ) if ( IsDevice( $deviceAlias, "ROOMMATE|GUEST" ) );
  616. }
  617. $msg = lc($entry) . " OK";
  618. $msg .= "\ndevice=$device id=$id lat=$lat long=$long trig=lc($entry)"
  619. if ( lc($entry) eq "test" );
  620. return ( "text/plain; charset=utf-8", $msg );
  621. }
  622. sub GEOFANCY_addExtension($$$) {
  623. my ( $name, $func, $link ) = @_;
  624. my $url = "/$link";
  625. Log3 $name, 2, "Registering GEOFANCY $name for URL $url...";
  626. $data{FWEXT}{$url}{deviceName} = $name;
  627. $data{FWEXT}{$url}{FUNC} = $func;
  628. $data{FWEXT}{$url}{LINK} = $link;
  629. }
  630. sub GEOFANCY_removeExtension($) {
  631. my ($link) = @_;
  632. my $url = "/$link";
  633. my $name = $data{FWEXT}{$url}{deviceName};
  634. Log3 $name, 2, "Unregistering GEOFANCY $name for URL $url...";
  635. delete $data{FWEXT}{$url};
  636. }
  637. sub GEOFANCY_ISO8601UTCtoLocal ($) {
  638. my ($datetime) = @_;
  639. $datetime =~ s/T/ /g if ( defined( $datetime && $datetime ne "" ) );
  640. $datetime =~ s/Z//g if ( defined( $datetime && $datetime ne "" ) );
  641. my (
  642. $date, $time, $y, $m, $d, $hour,
  643. $min, $sec, $hours, $minutes, $seconds, $timestamp
  644. );
  645. ( $date, $time ) = split( ' ', $datetime );
  646. ( $y, $m, $d ) = split( '-', $date );
  647. ( $hour, $min, $sec ) = split( ':', $time );
  648. $m -= 01;
  649. $timestamp = timegm( $sec, $min, $hour, $d, $m, $y );
  650. ( $sec, $min, $hour, $d, $m, $y ) = localtime($timestamp);
  651. $timestamp = timelocal( $sec, $min, $hour, $d, $m, $y );
  652. return $timestamp;
  653. }
  654. 1;
  655. =pod
  656. =item helper
  657. =item summary Geofencing for specific iOS, Android or Windows 10 apps
  658. =item summary_DE Geofencing f&uuml;r spezielle iOS, Android und Windows 10 Apps
  659. =begin html
  660. <p>
  661. <a name="GEOFANCY" id="GEOFANCY"></a>
  662. </p>
  663. <h3>
  664. GEOFANCY
  665. </h3>
  666. <ul>
  667. <li>Provides a webhook receiver for geofencing, e.g. via the following apps:<br>
  668. <br>
  669. </li>
  670. <li>
  671. <a href="https://itunes.apple.com/app/id615538630">Geofency (iOS)</a>
  672. </li>
  673. <li>
  674. <a href="https://itunes.apple.com/app/id725198453">Locative (iOS)</a>
  675. </li>
  676. <li>
  677. <a href="http://www.egigeozone.de">EgiGeoZone (Android)</a>
  678. </li>
  679. <li>
  680. <a href="https://www.microsoft.com/en-us/store/apps/smart-geofences/9nblggh4rk3k">SMART Geofences (Windows 10, Windows 10 Mobile)</a>
  681. </li>
  682. <li>
  683. <p>
  684. Note: GEOFANCY is an extension to <a href="#FHEMWEB">FHEMWEB</a>. You need to install FHEMWEB to use GEOFANCY.
  685. </p><a name="GEOFANCYdefine" id="GEOFANCYdefine"></a> <b>Define</b>
  686. <ul>
  687. <code>define &lt;name&gt; GEOFANCY &lt;infix&gt;</code><br>
  688. <br>
  689. Defines the webhook server. <code>&lt;infix&gt;</code> is the portion behind the FHEMWEB base URL (usually <code>http://hostname:8083/fhem</code>)<br>
  690. <br>
  691. Example:
  692. <ul>
  693. <code>define geofancy GEOFANCY geo</code><br>
  694. </ul><br>
  695. The webhook will be reachable at http://hostname:8083/fhem/geo in that case.<br>
  696. <br>
  697. </ul><a name="GEOFANCYset" id="GEOFANCYset"></a> <b>Set</b>
  698. <ul>
  699. <li>
  700. <b>clear</b> &nbsp;&nbsp;readings&nbsp;&nbsp; can be used to cleanup auto-created readings from deprecated devices.
  701. </li>
  702. </ul><br>
  703. <br>
  704. <a name="GEOFANCYattr" id="GEOFANCYattr"></a> <b>Attributes</b><br>
  705. <br>
  706. <ul>
  707. <li>devAlias: Mandatory attribute to assign device name alias to an UUID in the format DEVICEUUID:Aliasname (most readings will only be created if devAlias was defined).<br>
  708. Separate using <i>blank</i> to rename multiple device UUIDs.<br>
  709. <br>
  710. Should you be using GEOFANCY together with <a href="#ROOMMATE">ROOMMATE</a> or <a href="#GUEST">GUEST</a> you might consider using attribute r*_geofenceUUIDs directly at those devices instead.
  711. </li>
  712. </ul><br>
  713. <br>
  714. <b>Usage information / Hints on Security</b><br>
  715. <br>
  716. <ul>
  717. Likely your FHEM installation is not reachable directly from the internet (good idea!).<br>
  718. It is recommended to have a reverse proxy like <a href="http://loredo.me/post/116633549315/geeking-out-with-haproxy-on-pfsense-the-ultimate">HAproxy</a>, <a href="http://www.apsis.ch/pound/">Pound</a> or <a href="https://www.varnish-cache.org/">Varnish</a> in front of FHEM where you can make sure access is only possible to a specific URI like /fhem/geo. Apache or Nginx might do as well. However, in case you have Apache or Nginx running already you should still consider one of the named reverse proxies in front of it for fine-grain security configuration.<br>
  719. <br>
  720. You might also want to think about protecting the access by using HTTP Basic Authentication and encryption via TLS/SSL. Using TLS offloading in the reverse proxy software is highly recommended and software like HAproxy provides high control of data flow for TLS.<br>
  721. <br>
  722. Also the definition of a dedicated FHEMWEB instance for that purpose together with <a href="#allowed">allowed</a> might help to restrict FHEM's functionality (e.g. set attributes allowedCommands and allowedDevices to ",". Note that attributes <i>hiddengroup</i> and <i>hiddenroom</i> of FHEMWEB do NOT protect from just guessing/knowing the correct URI but would help tremendously to prevent easy inspection of your FHEM setup.)<br>
  723. <br>
  724. To make that reverse proxy available from the internet, just forward the appropriate port via your internet router.<br>
  725. <br>
  726. The actual solution on how you can securely make your GEOFANCY webhook available to the internet is not part of this documentation and depends on your own skills.
  727. </ul><br>
  728. <br>
  729. <b>Integration with Home Automation</b><br>
  730. <br>
  731. <ul>
  732. You might want to have a look to the module family of <a href="#ROOMMATE">ROOMMATE</a>, <a href="#GUEST">GUEST</a> and <a href="#RESIDENTS">RESIDENTS</a> for an easy processing of GEOFANCY events.
  733. </ul>
  734. </li>
  735. </ul>
  736. =end html
  737. =begin html_DE
  738. <p>
  739. <a name="GEOFANCY" id="GEOFANCY"></a>
  740. </p>
  741. <h3>
  742. GEOFANCY
  743. </h3>
  744. <ul>
  745. Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
  746. </ul>
  747. <ul>
  748. <a href='http://fhem.de/commandref.html#GEOFANCY'>GEOFANCY</a>
  749. </ul>
  750. =end html_DE
  751. =cut