diff --git a/fhem/contrib/DoorPi/70_DoorPi.pm b/fhem/contrib/DoorPi/70_DoorPi.pm new file mode 100644 index 000000000..3b429d6e5 --- /dev/null +++ b/fhem/contrib/DoorPi/70_DoorPi.pm @@ -0,0 +1,788 @@ +######################################################################################## +# +# DoorPi.pm +# +# FHEM module to communite with a Raspberry Pi door station +# +# Prof. Dr. Peter A. Henning, 2016 +# +# Version 0.2 - April 2016 +# +######################################################################################## +# +# This programm 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. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# A copy is found in the textfile GPL.txt and important notices to the license +# from the author is found in LICENSE.txt distributed with these scripts. +# +# This script 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. +# +######################################################################################## +package main; + +use strict; +use warnings; + +use JSON; # imports encode_json, decode_json, to_json and from_json. + +use vars qw{%attr %defs}; + +sub Log($$); + +#-- globals on start + +#-- these we may get on request +my %gets = ( + "config" => "C", + "history" => "H" +); + + +######################################################################################## +# +# DoorPi_Initialize +# +# Parameter hash +# +######################################################################################## + +sub DoorPi_Initialize ($) { + my ($hash) = @_; + + $hash->{DefFn} = "DoorPi_Define"; + $hash->{UndefFn} = "DoorPi_Undef"; + $hash->{AttrFn} = "DoorPi_Attr"; + $hash->{GetFn} = "DoorPi_Get"; + $hash->{SetFn} = "DoorPi_Set"; + #$hash->{NotifyFn} = "DoorPi_Notify"; + $hash->{InitFn} = "DoorPi_Init"; + + $hash->{AttrList}= "verbose ". + "language:de,en ". + "doorbutton dooropencmd doorlockcmd doorunlockcmd ". + "lightbutton lightoncmd lighttimercmd lightoffcmd ". + $readingFnAttributes; + + $hash->{FW_detailFn} = "DoorPi_makeTable"; + $hash->{FW_summaryFn} = "DoorPi_makeTable"; + $hash->{FW_atPageEnd} = 1; +} + +######################################################################################## +# +# DoorPi_Define - Implements DefFn function +# +# Parameter hash, definition string +# +######################################################################################## + +sub DoorPi_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "[DoorPi] Define the IP address of DoorPi as a parameter" + if(@a != 3); + return "[DoorPi] Invalid IP address of DoorPi" + if( $a[2] !~ m|\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?(\:\d+)?| ); + + my $dev = $a[2]; + #-- split into parts + my @tcp = split(':',$dev); + #-- when the specified ip address contains a port already, use it as supplied + if ( $tcp[1] ){ + $hash->{TCPIP} = $dev; + }else{ + $hash->{TCPIP} = $tcp[0].":80"; + }; + + @{$hash->{DATA}} = (); + @{$hash->{CMDS}} = (); + + $modules{DoorPi}{defptr}{$a[0]} = $hash; + + #-- InternalTimer blocks if init_done is not true + my $oid = $init_done; + $init_done = 1; + readingsSingleUpdate($hash,"state","initialized",1); + + DoorPi_GetConfig($hash); + DoorPi_GetHistory($hash); + $init_done = $oid; + return undef; +} + +####################################################################################### +# +# DoorPi_Undef - Implements UndefFn function +# +# Parameter hash = hash of device addressed +# +####################################################################################### + +sub DoorPi_Undef ($) { + my ($hash) = @_; + delete($modules{DoorPi}{defptr}{NAME}); + RemoveInternalTimer($hash); + return undef; +} + +####################################################################################### +# +# DoorPi_Attr - Set one attribute value +# +# Parameter hash = hash of device addressed +# a = argument array +# +######################################################################################## + +sub DoorPi_Attr(@) { + my ($do,$name,$key,$value) = @_; + + my $hash = $main::defs{$name}; + my $ret; + + if ( $do eq "set") { + ARGUMENT_HANDLER: { + $key eq "interval" and do { + $hash->{interval} = $value; + if ($main::init_done) { + # WHAT ? + } + last; + }; + } + } + return $ret; +} + +######################################################################################## +# +# DoorPi_Get - Implements GetFn function +# +# Parameter hash, argument array +# +######################################################################################## + +sub DoorPi_Get ($@) { + my ($hash, @a) = @_; + + #-- for the selector: which values are possible + #if ($a[1] eq "?"){ + # my $newkeys = join(" ",sort keys %gets); + # Log 1,"=====> newkeys before subs $newkeys"; + # $newkeys =~ s/config/config:noArg/g; # FHEMWEB sugar + # $newkeys =~ s/history/history:noArg/g; # FHEMWEB sugar + # Log 1,"=====> newkeys after subs $newkeys"; + # return $newkeys; + #} + + #-- check syntax + return "[DoorPi] DoorPi_Get needs exactly one parameter" if(@a != 2); + my $name = $hash->{NAME}; + my $v; + + #-- current configuration + if($a[1] eq "config") { + $v = DoorPi_GetConfig($hash); + if(defined($v)) { + Log GetLogLevel($name,2), "DoorPi_Get $a[1] error"; + return "$a[0] $a[1] => Error"; + } + $v = "OK"; + #-- current reading + }elsif($a[1] eq "history") { + $v = DoorPi_GetHistory($hash); + if(defined($v)) { + Log GetLogLevel($name,2), "DoorPi_Get $a[1] error"; + return "$a[0] $a[1] => Error"; + } + $v = "OK"; + } else { + return "DoorPi_Get with unknown argument $a[1], choose one of " . join(" ", sort keys %gets); + } + + Log GetLogLevel($name,3), "DoorPi_Get $a[1] $v"; + return "$a[0] $a[1] => $v"; +} + +######################################################################################## +# +# DoorPi_Set - Implements SetFn function +# +# Parameter hash, a = argument array +# +######################################################################################## + +sub DoorPi_Set ($@) { + my ($hash, @a) = @_; + my $name = shift @a; + my ($key,$value,$res); + + #-- commands + my $door = AttrVal($name, "doorbutton", "door"); + my $doorsubs = "open"; + $doorsubs .= ",lock" + if(AttrVal($name, "doorlockcmd",undef)); + $doorsubs .= ",unlock" + if(AttrVal($name, "doorunlockcmd",undef)); + + my $light = AttrVal($name, "lightbutton", "light"); + + #-- for the selector: which values are possible + if ($a[0] eq "?"){ + my $newkeys = join(" ",@{ $hash->{CMDS} }); + #Log 1,"=====> newkeys before subs $newkeys"; + $newkeys =~ s/$door/$door:$doorsubs/g; # FHEMWEB sugar + $newkeys =~ s/$light/$light:on,on-for-timer,off/g; # FHEMWEB sugar + $newkeys =~ s/button(\d\d?)/button$1:noArg/g; # FHEMWEB sugar + $newkeys =~ s/purge/purge:noArg/g; # FHEMWEB sugar + #Log 1,"=====> newkeys after subs $newkeys ($door,$light)"; + return $newkeys; + } + + #return "[DoorPi_Set] With unknown argument $a[0], choose one of " . join(" ", @{$hash->{CMDS}}) + # if(!defined($sets{$a[0]})); + + $key = shift @a; + $value = shift @a; + + if( $key eq "door" ){ + if( $value eq "open" ){ + $res=DoorPi_Cmd($hash,"door"); + #Log 1,"[DoorPiCmd] open door has result $res"; + if(AttrVal($name, "dooropencmd",undef)){ + fhem(AttrVal($name, "dooropencmd",undef)); + } + } + }elsif( $key =~ /button(\d\d?)/){ + $res=DoorPi_Cmd($hash,$key); + Log 1,"[DoorPiCmd] $key has result $res"; + }elsif( $key eq "purge"){ + $res=DoorPi_Cmd($hash,"purge"); + Log 1,"[DoorPiCmd] purge has result $res"; + }elsif( $key eq "clear"){ + $res=DoorPi_Cmd($hash,"clear"); + Log 1,"[DoorPiCmd] clear has result $res"; + } + + #Log GetLogLevel($name,3), "DoorPi_Set $name ".join(" ",@a)." => $res"; + #return "DoorPi_Set $name ".join(" ",@a)." => $res"; +} + +####################################################################################### +# +# DoorPi_GetConfig - acts as callable program DoorPi_GetConfig($hash) +# and as callback program DoorPi_GetConfig($hash,$err,$status) +# +# Parameter hash, err, status +# +####################################################################################### + +sub DoorPi_GetConfig () { + my ($hash,$err,$status) = @_; + my $name = $hash->{NAME}; + my $url; + + #-- get configuration from doorpi + if ( !$hash ){ + Log 1,"[DoorPi_GetConfig] called without hash"; + return undef; + }elsif ( $hash && !$err && !$status ){ + $url = "http://".$hash->{TCPIP}."/status?module=config"; + Log 1,"[DoorPi_GetConfig] called with only hash => Issue a non-blocking call to $url"; + HttpUtils_NonblockingGet({ + url => $url, + callback=>sub($$$){ DoorPi_GetConfig($hash,$_[1],$_[2]) } + }); + return undef; + }elsif ( $hash && $err ){ + Log 1,"[DoorPi_GetConfig] called with only hash and error $err"; + return undef; + } + Log 1,"[DoorPi_GetConfig] has obtained data"; + + #-- crude test if this is valid JSON or some HTML page + if( substr($status,0,1) eq "<" ){ + Log 1,"[DoorPi_GetConfig] but data is invalid"; + return; + } + + my $json = JSON->new->utf8; + my $jhash0 = $json->decode( $status ); + + #-- decode config + my $keyboards = $jhash0->{"config"}->{"keyboards"}; + my $fskey; + my $fscmds; + foreach my $key (sort(keys $keyboards)) { + $fskey = $key + if( $keyboards->{$key} eq "filesystem"); + } + if($fskey){ + Log 1,"[DoorPi_GetConfig] keyboard \'filesystem\' defined as '$fskey'"; + $hash->{HELPER}->{vkeyboard}=$fskey; + $fscmds = $jhash0->{"config"}->{$fskey."_InputPins"}; + foreach my $key (sort(keys $fscmds)) { + push(@{ $hash->{CMDS}},$key); + } + }else{ + Log 1,"[DoorPi_GetConfig] Warning: No keyboard \"filesystem\" defined"; + }; + $hash->{HELPER}->{wwwpath} = $jhash0->{"config"}->{"DoorPiWeb"}->{"www"}; + + + #=================== Put into READINGS + ##readingsBeginUpdate($hash); + ##readingsBulkUpdate($hash,"status_time",$jhash0->{"status_time"}); + ##readingsBulkUpdate($hash,"number_calls",int(@{ $hash->{DATA}})); + ##readingsEndUpdate($hash,1); + + return undef; +} + +####################################################################################### +# +# DoorPi_GetHistory - acts as callable program DoorPi_GetHistory($hash) +# and as callback program DoorPi_GetHistory($hash,$err1,$status1) +# and as callback program DoorPi_GetHistory($hash,$err1,$status1,$err2,$status2) +# +# Parameter hash +# +####################################################################################### + +sub DoorPi_GetHistory () { + my ($hash,$err1,$status1,$err2,$status2) = @_; + my $name = $hash->{NAME}; + my $url; + + #-- obtain call history and snapshot history from doorpi + if ( !$hash ){ + Log 1,"[DoorPi_GetHistory] called without hash"; + return undef; + }elsif ( $hash && !$err1 && !$status1 && !$err2 && !$status2 ){ + $url = "http://".$hash->{TCPIP}."/status?module=history_event"; + Log 1,"[DoorPi_GetHistory] called with only hash => Issue a non-blocking call to $url"; + HttpUtils_NonblockingGet({ + url => $url, + callback=>sub($$$){ DoorPi_GetHistory($hash,$_[1],$_[2]) } + }); + return undef; + }elsif ( $hash && $err1 && !$status1 && !$err2 && !$status2 ){ + Log 1,"[DoorPi_GetHistory] called with only hash and error $err1"; + return undef; + }elsif ( $hash && !$err1 && $status1 && !$err2 && !$status2 ){ + $url = "http://".$hash->{TCPIP}."/status?module=history_snapshot"; + Log 1,"[DoorPi_GetHistory] called with hash and data from first call => Issue a non-blocking call to $url"; + HttpUtils_NonblockingGet({ + url => $url, + callback=>sub($$$){ DoorPi_GetHistory($hash,$err1,$status1,$_[1],$_[2]) } + }); + return undef; + }elsif ( $hash && !$err1 && $status1 && $err2){ + Log 1,"[DoorPi_GetHistory] called with with hash and data from first call and second error $err2"; + return undef; + } + Log 1,"[DoorPi_GetHistory] has obtained data in two calls"; + + #-- crude test if this is valid JSON or some HTML page + if( substr($status1,0,1) eq "<" ){ + Log 1,"[DoorPi_GetHistory] but data from first call is invalid"; + return; + } + if( substr($status2,0,1) eq "<" ){ + Log 1,"[DoorPi_GetHistory] but data from second call is invalid"; + return; + } + + my $json = JSON->new->utf8; + my $jhash0 = $json->decode( $status1 ); + my $khash0 = $json->decode( $status2 ); + + #-- decode call history + my @history_event = @{$jhash0->{"history_event"}}; + my @history_snapshot = @{$khash0->{"history_snapshot"}}; + my $call = ""; + + #-- clear list of calls + @{$hash->{DATA}} = (); + + #-- going backward through the calls + my ($callend,$calletime,$calletarget,$callstime,$callstarget,$callsnap,$callrecord,$callstring); + for (my $i=0; $i<@history_event; $i++) { + my $event = $history_event[$i]; + + if( $event->{"event_name"} eq "OnCallStateChange" ){ + my $status1 = $event->{"additional_infos"}; + #-- workaround for bug in DoorPi + $status1 =~ tr/'/"/; + my $jhash1 = from_json( $status1 ); + my $call_state = $jhash1->{"call_state"}; + #-- end of call + if( ($call eq "") && (($call_state == 18) || ($call_state == 13)) ){ + $call = "active"; + $callrecord = ""; + $callend = $jhash1->{"state"}; + $callend =~ s/Call //; + if( $callend eq "released" ){ + #-- check previous 4 events + for( my $j=1; $j<5; $j++ ){ + if( $history_event[$i+$j]->{"event_name"} eq "OnCallStateChange"){ + my $status2 = $history_event[$i+$j]->{"additional_infos"}; + #-- workaround for bug in DoorPi + $status2 =~ tr/'/"/; + my $jhash2 = from_json( $status2 ); + if( $jhash2->{"state"} eq "Busy Here" ){ + $callend = "busy"; + last; + }elsif( $jhash2->{"state"} eq "Call ended" ){ + $callend = "OK"; + last; + } + } + } + }elsif( $callend eq "terminated" ){ + if( $history_event[$i-1]->{"event_name"} eq "OnSipPhoneCallTimeoutNoResponse"){ + $callend = "no response"; + } + } + $calletime = $event->{"start_time"}; + $calletarget = $jhash1->{"remote_uri"}; + }elsif( ($call eq "active") && ($call_state == 2) ){ + $call = ""; + $callstime = $event->{"start_time"}; + $callstarget = $jhash1->{"remote_uri"}; + #-- + if( $calletarget ne $callstarget){ + Log 1,"[DoorPi_GetHistory] Found error in call history of target $calletarget"; + }else{ + #-- Format values + my $state = ""; + my ($sec, $min, $hour, $day,$month,$year,$wday) = (localtime($callstime))[0,1,2,3,4,5,6]; + $year += 1900; + my $monthn = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$month]; + $wday = ("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")[$wday]; + my $timestamp = sprintf("%s, %2d %s %d %02d:%02d:%02d", $wday,$day,$monthn,$year,$hour, $min, $sec); + my $number = $callstarget; + $number =~ s/sip://; + $number =~ s/\@.*//; + my $result = $callend; + my $duration = int(($calletime - $callstime)*10+0.5)/10; + + my $record = $callrecord; + $record =~ s/^.*records\///; + #-- workaround for buggy DoorPi + $record = sprintf("%d-%02d-%2d_%02d-%02d-%02d.wav", $year,($month+1),$day,$hour, $min, $sec) + if( $callend eq "OK"); + + #-- this is the snapshot file if taken at the same time + my $snapshot = sprintf("%d-%02d-%2d_%02d-%02d-%02d.jpg", $year,($month+1),$day,$hour, $min, $sec); + #-- check if it is present in the list of snapshots + my $found = 0; + for( my $i=0; $i<@history_snapshot; $i++){ + if( index($history_snapshot[$i],$snapshot) > -1){ + $found = 1; + last; + } + } + #-- if not, look for a file made a second later + if( $found == 0 ){ + ($sec, $min, $hour, $day,$month,$year,$wday) = (localtime($callstime+1))[0,1,2,3,4,5,6]; + $year += 1900; + $monthn = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")[$month]; + $wday = ("So", "Mo", "Di", "Mi", "Do", "Fr", "Sa")[$wday]; + + #-- this is the snapshot file if taken at the same time + $snapshot = sprintf("%d-%02d-%2d_%02d-%02d-%02d.jpg", $year,($month+1),$day,$hour, $min, $sec); + #-- check if it is present in the list of snapshots + $found = 0; + for( my $i=0; $i<@history_snapshot; $i++){ + if( index($history_snapshot[$i],$snapshot) > -1){ + $found = 1; + last; + } + } + if( $found == 0 ){ + Log 1,"[DoorPi_GetHistory] No snapshot found with $snapshot"; + } + } + + #-- store this + push(@{ $hash->{DATA}}, [$state,$timestamp,$number,$result,$duration,$snapshot,$record] ); + } + } + } + #-- other events during call active + if( ($call eq "active") && ($event->{"event_name"} eq "OnRecorderStarted") ){ + my $status3 = $event->{"additional_infos"}; + $status3 =~ tr/'/"/; + my $jhash1 = from_json( $status3 ); + $callrecord = $jhash1->{"last_record_filename"}; + } + } + + #=================== Put into READINGS + readingsBeginUpdate($hash); + #readingsBulkUpdate($hash,"status_time",$jhash0->{"status_time"}); + readingsBulkUpdate($hash,"number_calls",int(@{ $hash->{DATA}})); + readingsEndUpdate($hash,1); + + return undef; +} + +######################################################################################## +# +# DoorPi_Cmd - Write command to DoorPi. Acting as callable routine and as callback +# +# Parameter hash, cmd = command +# +######################################################################################## + + sub DoorPi_Cmd () { + my ($hash, $cmd, $data) = @_; + my $name = $hash->{NAME}; + + my $url; + + if ( $hash && !$data){ + $url = "http://".$hash->{TCPIP}."/control/trigger_event?". + "event_name=OnKeyPressed_".$hash->{HELPER}->{vkeyboard}.".". + $cmd."&event_source=doorpi.keyboard.from_filesystem"; + Log 1,"[DoorPi_Cmd] called with only hash => Issue a non-blocking call to $url"; + HttpUtils_NonblockingGet({ + url => $url, + callback=>sub($$$){ DoorPi_Cmd($hash,$_[1],$_[2]) } + }); + return undef; + } + Log 1,"[DoorPi_Cmd] has obtained data"; + + #-- crude test if this is valid JSON or some HTML page + if( substr($data,0,1) eq "<" ){ + Log 1,"[DoorPi_Cmd] but data is invalid"; + return; + } + + my $json = JSON->new->utf8; + my $jhash = $json->decode( $data ); + my $msg = $jhash->{'message'}; + my $suc = $jhash->{'success'}; + if( $suc ){ + return $msg; + } + return undef; +} + +####################################################################################### +# +# DoorPi_maketable +# +# FW_detailFn & FW_summaryFn handler for creating the html output in FHEMWEB +# +####################################################################################### + +sub DoorPi_makeTable($$$$){ + my ($FW_wname, $devname, $room, $extPage) = @_; + my $hash = $defs{$devname}; + return DoorPi_list2html($hash) +} + +####################################################################################### +# +# DoorPi_list2html creating the call list as html string or json array +# +####################################################################################### + +sub DoorPi_list2html($;$){ + my ($hash, $to_json) = @_; + return undef if( !$hash ); + + my $name = $hash->{NAME}; + my $wwwpath = $hash->{HELPER}->{wwwpath}; + my $alias = AttrVal($hash->{NAME}, "alias", $hash->{NAME}); + my ($state,$timestamp,$number,$result,$duration,$snapshot,$record,$nrecord); + + my $td_style = 'style="padding-left:6px;padding-right:6px;"'; + my @json_output = (); + my $line; + + my $old_locale = setlocale(LC_ALL); + + if(AttrVal($name, "language", "en") eq "de"){ + setlocale(LC_ALL, "de_DE.utf8"); + }else{ + setlocale(LC_ALL, "en_US.utf8"); + } + + my $ret .= "
| ';
+ $ret .= ' '.$alias.''.(IsDisabled($name) ? ' (disabled)' : '').' '
+ unless($FW_webArgs{"detail"});
+ $ret .= ' | ||||||||||||
| ";
+ #-- div tag to support inform updates
+ $ret .= ' ';
+ if( exists($hash->{DATA}) && (int(@{$hash->{DATA}}) > 0) ){
+ $ret .= ' ";
+ }else{
+ if(AttrVal($name, "language", "en") eq "de"){
+ $ret .= "leer";
+ }else{
+ $ret .= "empty";
+ }
+ }
+
+ $ret .= "
|
FHEM module to communicate with a Raspberry Pi door station running DoorPi
+
+ define DoorStation DoorPi 192.168.0.51
+
+
+ define <name> DoorPi <IP address>
+
Define a DoorpiPi instance.
+
<IP address>
+ set <name> purge set <name> clear get <name> config
+ get <name> history
+ attr <name> doorbutton
+ <string>
+ attr <name> dooropencmd
+ <string>
+ attr <name> doorlockcmd
+ <string>
+ attr <name> doorunlockcmd
+ <string>
+ attr <name> lightbutton
+ <string>
+