2014-01-08 23:53:52 +00:00
# $Id$
##############################################################################
#
# 98_GEOFANCY.pm
# An FHEM Perl module to receive geofencing webhooks from geofancy.com.
#
# Copyright by Julian Pawlowski
# e-mail: julian.pawlowski at gmail.com
#
# Based on HTTPSRV from Dr. Boris Neubert
#
# This file is part of fhem.
#
# Fhem is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# Fhem is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
#
#
2014-07-05 11:40:31 +00:00
# Version: 1.1.1
2014-01-08 23:53:52 +00:00
#
# Major Version History:
2014-02-07 13:04:29 +00:00
# - 1.1.0 - 2014-02-06
# -- Support for both apps: Geofency and Geofancy
#
2014-01-08 23:53:52 +00:00
# - 1.0.0 - 2014-01-09
# -- First release
#
##############################################################################
package main ;
use strict ;
use warnings ;
use vars qw( %data ) ;
use HttpUtils ;
2014-02-07 22:52:01 +00:00
use Time::Local ;
2014-01-08 23:53:52 +00:00
use Data::Dumper ;
sub GEOFANCY_Set ($@) ;
sub GEOFANCY_Define ($$) ;
sub GEOFANCY_Undefine ($$) ;
#########################
sub GEOFANCY_addExtension ($$$) {
my ( $ name , $ func , $ link ) = @ _ ;
my $ url = "/$link" ;
2015-03-08 11:52:08 +00:00
Log3 $ name , 2 , "Registering GEOFANCY $name for URL $url..." ;
2014-01-08 23:53:52 +00:00
$ data { FWEXT } { $ url } { deviceName } = $ name ;
$ data { FWEXT } { $ url } { FUNC } = $ func ;
$ data { FWEXT } { $ url } { LINK } = $ link ;
}
2014-07-05 11:40:31 +00:00
#########################
2014-01-08 23:53:52 +00:00
sub GEOFANCY_removeExtension ($) {
my ( $ link ) = @ _ ;
my $ url = "/$link" ;
my $ name = $ data { FWEXT } { $ url } { deviceName } ;
2015-03-08 11:52:08 +00:00
Log3 $ name , 2 , "Unregistering GEOFANCY $name for URL $url..." ;
2014-01-08 23:53:52 +00:00
delete $ data { FWEXT } { $ url } ;
}
###################################
sub GEOFANCY_Initialize ($) {
my ( $ hash ) = @ _ ;
Log3 $ hash , 5 , "GEOFANCY_Initialize: Entering" ;
$ hash - > { SetFn } = "GEOFANCY_Set" ;
$ hash - > { DefFn } = "GEOFANCY_Define" ;
$ hash - > { UndefFn } = "GEOFANCY_Undefine" ;
$ hash - > { AttrList } = "devAlias " . $ readingFnAttributes ;
}
###################################
sub GEOFANCY_Define ($$) {
my ( $ hash , $ def ) = @ _ ;
my @ a = split ( "[ \t]+" , $ def , 5 ) ;
return "Usage: define <name> GEOFANCY <infix>"
if ( int ( @ a ) != 3 ) ;
my $ name = $ a [ 0 ] ;
my $ infix = $ a [ 2 ] ;
$ hash - > { fhem } { infix } = $ infix ;
GEOFANCY_addExtension ( $ name , "GEOFANCY_CGI" , $ infix ) ;
readingsBeginUpdate ( $ hash ) ;
readingsBulkUpdate ( $ hash , "state" , "initialized" ) ;
readingsEndUpdate ( $ hash , 1 ) ;
return undef ;
}
###################################
sub GEOFANCY_Undefine ($$) {
my ( $ hash , $ name ) = @ _ ;
GEOFANCY_removeExtension ( $ hash - > { fhem } { infix } ) ;
return undef ;
}
###################################
sub GEOFANCY_Set ($@) {
my ( $ hash , @ a ) = @ _ ;
my $ name = $ hash - > { NAME } ;
my $ state = $ hash - > { STATE } ;
Log3 $ name , 5 , "GEOFANCY $name: called function GEOFANCY_Set()" ;
return "No Argument given" if ( ! defined ( $ a [ 1 ] ) ) ;
my $ usage = "Unknown argument " . $ a [ 1 ] . ", choose one of clear:readings" ;
# clear
if ( $ a [ 1 ] eq "clear" ) {
Log3 $ name , 2 , "GEOFANCY set $name " . $ a [ 1 ] ;
if ( $ a [ 2 ] ) {
# readings
if ( $ a [ 2 ] eq "readings" ) {
delete $ hash - > { READINGS } ;
readingsBeginUpdate ( $ hash ) ;
readingsBulkUpdate ( $ hash , "state" , "clearedReadings" ) ;
readingsEndUpdate ( $ hash , 1 ) ;
}
}
else {
return "No Argument given, choose one of readings " ;
}
}
# return usage hint
else {
return $ usage ;
}
return undef ;
}
############################################################################################################
#
# Begin of helper functions
#
############################################################################################################
###################################
sub GEOFANCY_CGI () {
2014-02-07 13:04:29 +00:00
# Geofancy
# /$infix?device=UUIDdev&id=UUIDloc&latitude=xx.x&longitude=xx.x&trigger=(enter|exit)
#
# Geofency
# /$infix?id=UUIDloc&name=locName&entry=(1|0)&date=DATE&latitude=xx.x&longitude=xx.x&device=UUIDdev
2014-01-08 23:53:52 +00:00
my ( $ request ) = @ _ ;
my $ hash ;
2014-07-05 11:40:31 +00:00
my $ name = "" ;
my $ link = "" ;
my $ URI = "" ;
my $ device = "" ;
my $ id = "" ;
my $ lat = "" ;
my $ long = "" ;
my $ entry = "" ;
my $ msg = "" ;
my $ date = "" ;
my $ time = "" ;
my $ locName = "" ;
2014-01-08 23:53:52 +00:00
2014-01-09 13:22:00 +00:00
# data received
2014-01-11 11:35:10 +00:00
if ( $ request =~ m , ^ ( /[^/ ] + ? ) ( ? : \ & | \ ? ) ( . * ) ? $, ) {
2014-01-08 23:53:52 +00:00
$ link = $ 1 ;
$ URI = $ 2 ;
# get device name
$ name = $ data { FWEXT } { $ link } { deviceName } if ( $ data { FWEXT } { $ link } ) ;
# return error if no such device
return ( "text/plain; charset=utf-8" ,
"NOK No GEOFANCY device for webhook $link" )
unless ( $ name ) ;
# extract values from URI
my $ webArgs ;
foreach my $ pv ( split ( "&" , $ URI ) ) {
next if ( $ pv eq "" ) ;
$ pv =~ s/\+/ /g ;
$ pv =~ s/%([\dA-F][\dA-F])/chr(hex($1))/ige ;
my ( $ p , $ v ) = split ( "=" , $ pv , 2 ) ;
$ webArgs - > { $ p } = $ v ;
}
2014-02-07 13:04:29 +00:00
if (
! defined ( $ webArgs - > { device } )
2014-01-08 23:53:52 +00:00
|| ! defined ( $ webArgs - > { id } )
2014-02-07 13:04:29 +00:00
|| (
! (
defined ( $ webArgs - > { trigger } && $ webArgs - > { trigger } ne "" )
)
&& ! ( defined ( $ webArgs - > { entry } ) && $ webArgs - > { entry } ne "" )
)
2014-01-08 23:53:52 +00:00
|| $ webArgs - > { device } eq ""
|| $ webArgs - > { id } eq ""
2014-02-07 13:04:29 +00:00
)
2014-01-08 23:53:52 +00:00
{
2014-02-07 13:04:29 +00:00
$ msg = " id=" ;
2014-01-08 23:53:52 +00:00
$ msg . = $ webArgs - > { id } if ( $ webArgs - > { id } ) ;
2014-02-07 13:04:29 +00:00
$ msg . = " name=" ;
$ msg . = $ webArgs - > { name } if ( $ webArgs - > { name } ) ;
$ msg . = " entry=" ;
$ msg . = $ webArgs - > { entry } if ( $ webArgs - > { entry } ) ;
$ msg . = " trigger=" ;
$ msg . = $ webArgs - > { trigger } if ( $ webArgs - > { trigger } ) ;
$ msg . = " date=" ;
$ msg . = $ webArgs - > { date } if ( $ webArgs - > { date } ) ;
2014-01-08 23:53:52 +00:00
$ msg . = " latitude=" ;
$ msg . = $ webArgs - > { latitude } if ( $ webArgs - > { latitude } ) ;
$ msg . = " longitude=" ;
$ msg . = $ webArgs - > { longitude } if ( $ webArgs - > { longitude } ) ;
2014-02-07 13:04:29 +00:00
$ msg . = " device=" ;
$ msg . = $ webArgs - > { device } if ( $ webArgs - > { device } ) ;
2014-01-08 23:53:52 +00:00
Log3 $ name , 3 ,
"GEOFANCY: Insufficient data received for webhook $link:\n"
. $ msg ;
return ( "text/plain; charset=utf-8" ,
"NOK\nInsufficient data received for webhook $link:\n" . $ msg ) ;
}
2014-02-07 13:04:29 +00:00
# Geofancy.app
if ( defined $ webArgs - > { trigger } ) {
$ id = $ webArgs - > { id } ;
$ entry = $ webArgs - > { trigger } ;
$ lat = $ webArgs - > { latitude } ;
$ long = $ webArgs - > { longitude } ;
$ device = $ webArgs - > { device } ;
}
# Geofency.app
elsif ( defined $ webArgs - > { entry } ) {
$ id = $ webArgs - > { id } ;
$ locName = $ webArgs - > { name } ;
$ entry = $ webArgs - > { entry } ;
$ date = $ webArgs - > { date } ;
$ lat = $ webArgs - > { latitude } ;
$ long = $ webArgs - > { longitude } ;
$ device = $ webArgs - > { device } ;
}
else {
return "fatal error" ;
}
2014-01-08 23:53:52 +00:00
}
2014-01-09 13:22:00 +00:00
# no data received
2014-01-08 23:53:52 +00:00
else {
2014-02-07 13:04:29 +00:00
Log3 undef , 3 , "GEOFANCY: No data received" ;
2014-01-08 23:53:52 +00:00
2014-02-07 13:04:29 +00:00
return ( "text/plain; charset=utf-8" , "NOK No data received" ) ;
2014-01-08 23:53:52 +00:00
}
# return error if unknown trigger
2014-02-07 13:04:29 +00:00
return ( "text/plain; charset=utf-8" , "$entry NOK" )
if ( $ entry ne "enter"
&& $ entry ne "1"
&& $ entry ne "exit"
&& $ entry ne "0"
&& $ entry ne "test" ) ;
2014-01-08 23:53:52 +00:00
$ hash = $ defs { $ name } ;
# Device alias handling
#
delete $ hash - > { helper } { device_aliases }
if $ hash - > { helper } { device_aliases } ;
delete $ hash - > { helper } { device_names }
if $ hash - > { helper } { device_names } ;
if ( defined ( $ attr { $ name } { devAlias } ) ) {
my @ devices = split ( ' ' , $ attr { $ name } { devAlias } ) ;
if ( @ devices ) {
foreach ( @ devices ) {
my @ device = split ( ':' , $ _ ) ;
$ hash - > { helper } { device_aliases } { $ device [ 0 ] } =
$ device [ 1 ] ;
$ hash - > { helper } { device_names } { $ device [ 1 ] } =
$ device [ 0 ] ;
}
}
}
$ device = $ hash - > { helper } { device_aliases } { $ device }
if $ hash - > { helper } { device_aliases } { $ device } ;
Log3 $ name , 4 ,
2014-02-07 13:04:29 +00:00
"GEOFANCY $name: id=$id name=$locName entry=$entry date=$date lat=$lat long=$long dev=$device" ;
2014-01-08 23:53:52 +00:00
readingsBeginUpdate ( $ hash ) ;
2014-01-09 13:22:00 +00:00
2014-02-07 13:04:29 +00:00
# use time from device
if ( defined $ date && $ date ne "" ) {
$ hash - > { ".updateTime" } = GEOFANCY_ISO8601UTCtoLocal ( $ date ) ;
$ hash - > { ".updateTimestamp" } = FmtDateTime ( $ hash - > { ".updateTime" } ) ;
$ time = $ hash - > { ".updateTimestamp" } ;
}
# use local FHEM time
else {
$ time = TimeNow ( ) ;
}
2014-01-09 13:22:00 +00:00
# General readings
readingsBulkUpdate ( $ hash , "state" ,
2014-02-07 18:18:39 +00:00
"id:$id name:$locName trig:$entry date:$date lat:$lat long:$long dev:$device"
2014-02-07 13:04:29 +00:00
) ;
$ id = $ locName if ( defined ( $ locName ) && $ locName ne "" ) ;
2014-01-08 23:53:52 +00:00
readingsBulkUpdate ( $ hash , "lastDevice" , $ device ) ;
2014-01-09 13:22:00 +00:00
readingsBulkUpdate ( $ hash , "lastArr" , $ device . " " . $ id )
2014-02-07 13:04:29 +00:00
if ( $ entry eq "enter" || $ entry eq "1" ) ;
2014-01-09 13:22:00 +00:00
readingsBulkUpdate ( $ hash , "lastDep" , $ device . " " . $ id )
2014-02-07 13:04:29 +00:00
if ( $ entry eq "exit" || $ entry eq "0" ) ;
2014-01-09 13:22:00 +00:00
2014-02-07 13:04:29 +00:00
if ( $ entry eq "enter" || $ entry eq "1" || $ entry eq "test" ) {
2015-03-08 11:52:08 +00:00
Log3 $ name , 4 , "GEOFANCY $name: $device arrived at $id" ;
2014-01-09 13:22:00 +00:00
readingsBulkUpdate ( $ hash , $ device , "arrived " . $ id ) ;
readingsBulkUpdate ( $ hash , "currLoc_" . $ device , $ id ) ;
readingsBulkUpdate ( $ hash , "currLocLat_" . $ device , $ lat ) ;
readingsBulkUpdate ( $ hash , "currLocLong_" . $ device , $ long ) ;
readingsBulkUpdate ( $ hash , "currLocTime_" . $ device , $ time ) ;
}
2014-02-07 13:04:29 +00:00
if ( $ entry eq "exit" || $ entry eq "0" ) {
2014-01-09 13:22:00 +00:00
my $ currReading ;
my $ lastReading ;
2015-03-08 11:52:08 +00:00
Log3 $ name , 4 , "GEOFANCY $name: $device left $id and is underway" ;
2014-01-09 13:22:00 +00:00
# backup last known location if not "underway"
$ currReading = "currLoc_" . $ device ;
if ( defined ( $ hash - > { READINGS } { $ currReading } { VAL } )
&& $ hash - > { READINGS } { $ currReading } { VAL } ne "underway" )
{
foreach ( 'Loc' , 'LocLat' , 'LocLong' ) {
$ currReading = "curr" . $ _ . "_" . $ device ;
$ lastReading = "last" . $ _ . "_" . $ device ;
readingsBulkUpdate ( $ hash , $ lastReading ,
$ hash - > { READINGS } { $ currReading } { VAL } )
if ( defined ( $ hash - > { READINGS } { $ currReading } { VAL } ) ) ;
}
$ currReading = "currLocTime_" . $ device ;
readingsBulkUpdate (
$ hash ,
"lastLocArr_" . $ device ,
$ hash - > { READINGS } { $ currReading } { VAL }
) if ( defined ( $ hash - > { READINGS } { $ currReading } { VAL } ) ) ;
readingsBulkUpdate ( $ hash , "lastLocDep_" . $ device , $ time ) ;
}
readingsBulkUpdate ( $ hash , $ device , "left " . $ id ) ;
readingsBulkUpdate ( $ hash , "currLoc_" . $ device , "underway" ) ;
readingsBulkUpdate ( $ hash , "currLocLat_" . $ device , "-" ) ;
readingsBulkUpdate ( $ hash , "currLocLong_" . $ device , "-" ) ;
readingsBulkUpdate ( $ hash , "currLocTime_" . $ device , $ time ) ;
}
2014-01-08 23:53:52 +00:00
readingsEndUpdate ( $ hash , 1 ) ;
2014-02-07 13:04:29 +00:00
$ msg = "$entry OK" ;
$ msg . = "\ndevice=$device id=$id lat=$lat long=$long trigger=$entry"
if ( $ entry eq "test" ) ;
2014-01-08 23:53:52 +00:00
return ( "text/plain; charset=utf-8" , $ msg ) ;
}
2014-02-07 13:04:29 +00:00
sub GEOFANCY_ISO8601UTCtoLocal ($) {
my ( $ datetime ) = @ _ ;
$ datetime =~ s/T/ /g if ( defined ( $ datetime && $ datetime ne "" ) ) ;
$ datetime =~ s/Z//g if ( defined ( $ datetime && $ datetime ne "" ) ) ;
my (
$ date , $ time , $ y , $ m , $ d , $ hour ,
$ min , $ sec , $ hours , $ minutes , $ seconds , $ timestamp
) ;
( $ date , $ time ) = split ( ' ' , $ datetime ) ;
( $ y , $ m , $ d ) = split ( '-' , $ date ) ;
( $ hour , $ min , $ sec ) = split ( ':' , $ time ) ;
$ m -= 01 ;
$ timestamp = timegm ( $ sec , $ min , $ hour , $ d , $ m , $ y ) ;
( $ sec , $ min , $ hour , $ d , $ m , $ y ) = localtime ( $ timestamp ) ;
$ timestamp = timelocal ( $ sec , $ min , $ hour , $ d , $ m , $ y ) ;
return $ timestamp ;
}
2014-01-08 23:53:52 +00:00
1 ;
= pod
2014-02-28 12:40:20 +00:00
2014-01-08 23:53:52 +00:00
= begin html
2014-02-28 12:40:20 +00:00
<p>
< a name = "GEOFANCY" id = "GEOFANCY" > </a>
</p>
<h3>
GEOFANCY
</h3>
2014-01-08 23:53:52 +00:00
<ul>
2014-03-01 15:03:39 +00:00
<li> Provides webhook receiver for geofencing via the following iOS apps: <br>
2014-02-28 12:40:20 +00:00
<br>
</li>
<li>
< a href = "https://itunes.apple.com/de/app/geofency-time-tracking-automatic/id615538630?l=en&mt=8" > Geofency </a>
</li>
<li>
< a href = "https://itunes.apple.com/de/app/geofancy/id725198453?l=en&mt=8" > Geofancy </a>
</li>
2014-03-01 15:03:39 +00:00
<li>
2014-02-28 12:40:20 +00:00
<p>
Note: GEOFANCY is an extension to < a href = "FHEMWEB" > FHEMWEB </a> . You need to install FHEMWEB to use GEOFANCY .
</p> < a name = "GEOFANCYdefine" id = "GEOFANCYdefine" > </a> <b> Define </b>
< div style = "margin-left: 2em" >
<code> define & lt ; name & gt ; & lt ; infix & gt ; </code> <br>
<br>
Defines the webhook server . <code> & lt ; infix & gt ; </code> is the portion behind the FHEMWEB base URL ( usually <code> http: // hostname:8083 /fhem</co de > ) <br>
<br>
Example:
< div style = "margin-left: 2em" >
<code> define geofancy GEOFANCY geo </code> <br>
</div> <br>
The webhook will be reachable at http: // hostname:8083 /fhem/g eo in that case . <br>
<br>
</div> < a name = "GEOFANCYset" id = "GEOFANCYset" > </a> <b> Set </b>
<ul>
<li>
<b> clear </b> & nbsp ; & nbsp ; readings & nbsp ; & nbsp ; can be used to cleanup auto - created readings from deprecated devices .
</li>
</ul> <br>
<br>
< a name = "GEOFANCYattr" id = "GEOFANCYattr" > </a> <b> Attributes </b> <br>
<br>
<ul>
<li> devAlias: can be used to rename device names in the format DEVICEUUID:Aliasname . Separate using blank to rename multiple devices .
</li>
</ul> <br>
<br>
<b> Usage information </b> <br>
<br>
< div style = "margin-left: 2em" >
Likely your FHEM installation is not reachable directly from the internet ( good idea ! ) . <br>
It is recommended to have a reverse proxy like nginx or Apache in front of FHEM where you can make sure access is only possible to specific subdirectories like /fhem/g eo . <br>
You might also want to think about protecting the access by using HTTP Basic Authentication and encryption via SSL . <br>
Also the definition of a dedicated FHEMWEB instance for that purpose might help to restrict FHEM 's functionality (note that the ' hidden ' attributes of FHEMWEB currently do NOT protect from just guessing / knowing the correct URL ! ) <br>
<br>
To make that reverse proxy available from the internet , just forward the appropriate port via your internet router . <br>
<br>
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 .
</div> <br>
<br>
<b> Integration with Home Automation </b> <br>
<br>
< div style = "margin-left: 2em" >
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 .
</div>
</li>
</ul>
2014-01-08 23:53:52 +00:00
= end html
2014-02-02 11:13:46 +00:00
= begin html_DE
2014-02-03 07:27:20 +00:00
2014-02-28 12:40:20 +00:00
<p>
< a name = "GEOFANCY" id = "GEOFANCY" > </a>
</p>
<h3>
GEOFANCY
</h3>
< div style = "margin-left: 2em" >
Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden . Die englische Version ist hier zu finden:
</div>
< div style = "margin-left: 2em" >
< a href = 'http://fhem.de/commandref.html#GEOFANCY' > GEOFANCY </a>
</div>
2014-02-02 11:13:46 +00:00
= end html_DE
2014-02-28 12:40:20 +00:00
2014-01-08 23:53:52 +00:00
= cut