######################################################################################## # # Alarm.pm # # FHEM module to set up a house alarm system with 8 different alarm levels # # Prof. Dr. Peter A. Henning # # $Id$ # ######################################################################################## # # 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 vars qw(%defs); # FHEM device/button definitions use vars qw(%intAt); # FHEM at definitions use vars qw($FW_ME); use JSON; # imports encode_json, decode_json, to_json and from_json. ######################### # Global variables my $alarmlinkname = "Alarms"; # link text my $alarmhiddenroom = "AlarmRoom"; # hidden room my $alarmpublicroom = "Alarm"; # public room my $alarmno = 8; my $alarmversion = "5.0"; my %alarm_transtable_EN = ( "ok" => "OK", "notok" => "Not OK", "cond" => "Condition", "start" => "Start", "end" => "End", "autocan" => "AutoCancel", "status" => "Status", "notstarted" => "Not started", "next" => "Next", "arm" => "Arm", "disarm" => "Disarm", "armbutton" => "Arming", "disarmbutton" => "Disarming", "cancelbutton" => "Canceling", "raise" => "Raise", "wait" => "Wait", "delay" => "Delay", "cancel" => "Cancel", "button" => "Button", "level" => "Level", "message" => "Message", "messagepart" => "Message Part", "notify" => "Notify", "notifyto" => "Notify to", "notifyby" => "Notify by", "setby" => "Set by", "regexp" => "RegExp", "time" => "Time", "description" => "Description", "settings" => "Settings", "sensors" => "Sensors", "actors" => "Actors", "action" => "Action", "setaction" => "Set Action", "unsetaction" => "Unset Action", "testaction" => "Test", "armaction" => "Arm Action", "disarmaction" => "Disarm Action", "waitaction" => "Wait Action", "cancelaction" => "Cancel Action", "canceled" => "canceled by:", "alarm" => "Alarm", "raised" => "raised by:", "alarms" => "Alarm System", "setparms" => "Set Parameters", #-- "state" => "Security", "unlocked" => "Unlocked", "locked" => "Locked", "unsecured" => "Not Secured", "secured" => "Secured", "protected" => "Geschützt", "guarded" => "Guarded" ); my %alarm_transtable_DE = ( "ok" => "OK", "notok" => "Nicht OK", "cond" => "Bedingung", "start" => "Start", "end" => "Ende", "autocan" => "AutoWiderruf", "status" => "Status", "notstarted" => "Nicht gestartet", "next" => "Nächste", "arm" => "Schärfen", "disarm" => "Entschärfen", "armbutton" => "Schärfen", "disarmbutton" => "Entschärfen", "cancelbutton" => "Widerrufen", "raise" => "Auslösen", "wait" => "Warte", "delay" => "Verzögerung", "cancel" => "Widerruf", "button" => "Button", "level" => "Level", "message" => "Nachricht", "messagepart" => "Nachrichtenteil", "notify" => "Auslösung", "notifyto" => "Wirkt auf", "notifyby" => "Auslösung durch", "setby" => "Gesetzt durch", "regexp" => "RegExp", "time" => "Zeit", "description" => "Beschreibung", "settings" => "Einstellungen", "sensors" => "Sensoren", "actors" => "Aktoren", "action" => "Wirkung", "setaction" => "Aktion Setzen", "unsetaction" => "Aktion Rücksetzen", "testaction" => "Testen", "armaction" => "Scharf-Aktion", "disarmaction" => "Unscharf-Aktion", "waitaction" => "Warte-Aktion", "cancelaction" => "Widerruf-Aktion", "canceled" => "widerrufen durch:", "alarm" => "Alarm", "raised" => "ausgelöst durch:", "alarms" => "Alarmanlage", "setparms" => "Parameter setzen", #-- "state" => "Sicherheit", "unlocked" => "Unverschlossen", "locked" => "Verschlossen", "unsecured" => "Nicht Gesichert", "secured" => "Gesichert", "protected" => "Geschützt", "guarded" => "Überwacht" ); my $alarm_tt; ######################################################################################### # # Alarm_Initialize # # Parameter hash = hash of device addressed # ######################################################################################### sub Alarm_Initialize ($) { my ($hash) = @_; $hash->{DefFn} = "Alarm_Define"; $hash->{SetFn} = "Alarm_Set"; $hash->{GetFn} = "Alarm_Get"; $hash->{UndefFn} = "Alarm_Undef"; #$hash->{AttrFn} = "Alarm_Attr"; my $attst = "lockstate:locked,unlocked testbutton:0,1 statedisplay:simple,color,table,none noicons iconmap disarmcolor ". "armwaitcolor armcolor alarmcolor armdelay armwait armact disarmact cancelact"; for( my $level=0;$level<$alarmno;$level++ ){ $attst .=" level".$level."cond level".$level."start level".$level."end level".$level."autocan level".$level."msg level".$level."onact level".$level."offact "; } $hash->{AttrList} = $attst; if( !defined($alarm_tt) ){ #-- in any attribute redefinition readjust language my $lang = AttrVal("global","language","EN"); if( $lang eq "DE"){ $alarm_tt = \%alarm_transtable_DE; }else{ $alarm_tt = \%alarm_transtable_EN; } } $alarmlinkname = $alarm_tt->{"alarms"}; $data{FWEXT}{Alarmx}{LINK} = "?room=".$alarmhiddenroom; $data{FWEXT}{Alarmx}{NAME} = $alarmlinkname; $data{FWEXT}{"/Alarm_widget"}{FUNC} = "Alarm_widget"; $data{FWEXT}{"/Alarm_widget"}{FORKABLE} = 0; return undef; } ######################################################################################### # # Alarm_Define - Implements DefFn function # # Parameter hash = hash of device addressed, def = definition string # ######################################################################################### sub Alarm_Define ($$) { my ($hash, $def) = @_; my $now = time(); my $name = $hash->{NAME}; $hash->{VERSION} = $alarmversion; #-- readjust language my $lang = AttrVal("global","language","EN"); if( $lang eq "DE"){ $alarm_tt = \%alarm_transtable_DE; }else{ $alarm_tt = \%alarm_transtable_EN; } readingsSingleUpdate( $hash, "state", "Initialized", 1 ); $alarmhiddenroom = defined($attr{$name}{"hiddenroom"}) ? $attr{$name}{"hiddenroom"} : $alarmhiddenroom; $alarmpublicroom = defined($attr{$name}{"publicroom"}) ? $attr{$name}{"publicroom"} : $alarmpublicroom; $data{FWEXT}{Alarmx}{LINK} = "?room=".$alarmhiddenroom; $data{FWEXT}{Alarmx}{NAME} = $alarmlinkname; $attr{$name}{"room"} = $alarmhiddenroom; #$data{FWEXT}{"/Alarm_widget"}{FUNC} = "Alarm_widget"; #$data{FWEXT}{"/Alarm_widget"}{FORKABLE} = 0; my $date = Alarm_restore($hash,0); #-- data seems to be ok, restore if( defined($date) ){ Alarm_restore($hash,1); Log3 $name,1,"[Alarm_Define] data hash restored from save file with date $date"; #-- intialization }else{ for( my $i=0;$i<$alarmno;$i++){ $hash->{DATA}{"armstate"}{"level".$i} = "disarmed"; } Alarm_save($hash); Log3 $name,1,"[Alarm_Define] data hash is initialized"; } $modules{Alarm}{defptr}{$name} = $hash; RemoveInternalTimer($hash); InternalTimer ($now + 5, 'Alarm_CreateEntry', $hash, 0); return; } ######################################################################################### # # Alarm_Undef - Implements Undef function # # Parameter hash = hash of device addressed, def = definition string # ######################################################################################### sub Alarm_Undef ($$) { my ($hash,$arg) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); delete $data{FWEXT}{Alarmx}; if (defined $defs{$name."_weblink"}) { FW_fC("delete ".$name."_weblink"); Log3 $hash, 3, "[".$name. " V".$alarmversion."]"." Weblink ".$name."_weblink deleted"; } return undef; } ######################################################################################### # # Alarm_Attr - Implements Attr function # # Parameter hash = hash of device addressed, ??? # ######################################################################################### sub Alarm_Attr($$$) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{"$name"}; #-- in any attribute redefinition readjust language my $lang = AttrVal("global","language","EN"); if( $lang eq "DE"){ $alarm_tt = \%alarm_transtable_DE; }else{ $alarm_tt = \%alarm_transtable_EN; } return; } ######################################################################################### # # Alarm_CreateEntry - Puts the Alarm entry into the FHEM menu # # Parameter hash = hash of device addressed # ######################################################################################### sub Alarm_CreateEntry($) { my ($hash) = @_; my $name = $hash->{NAME}; if (!defined $defs{$name."_weblink"}) { FW_fC("define ".$name."_weblink weblink htmlCode {Alarm_Html(\"".$name."\")}"); Log3 $hash, 3, "[".$name. " V".$alarmversion."]"." Weblink ".$name."_weblink created"; } FW_fC("attr ".$name."_weblink room ".$alarmhiddenroom); foreach my $dn (sort keys %defs) { if ($defs{$dn}{TYPE} eq "FHEMWEB" && $defs{$dn}{NAME} !~ /FHEMWEB:/) { my $hr = AttrVal($defs{$dn}{NAME}, "hiddenroom", ""); if (index($hr,$alarmhiddenroom) == -1){ if ($hr eq "") { FW_fC("attr ".$defs{$dn}{NAME}." hiddenroom ".$alarmhiddenroom); }else { FW_fC("attr ".$defs{$dn}{NAME}." hiddenroom ".$hr.",".$alarmhiddenroom); } Log3 $hash, 3, "[".$name. " V".$alarmversion."]"." Added hidden room '".$alarmhiddenroom."' to ".$defs{$dn}{NAME}; } } } #-- recover state from stored readings readingsBeginUpdate($hash); for( my $level=0;$level<$alarmno;$level++ ){ my $val = $hash->{DATA}{"armstate"}{"level".$level}; readingsBulkUpdate( $hash, "level".$level, $val); } my $mga = Alarm_getstate($hash); readingsBulkUpdate( $hash, "state", $mga); readingsEndUpdate( $hash,1 ); } ######################################################################################### # # Alarm_Set - Implements the Set function # # Parameter hash = hash of device addressed # ######################################################################################### sub Alarm_Set($@) { my ( $hash, $name, $cmd, @args ) = @_; if ( $cmd =~ /^(cancel|arm|disarm)(ed)?$/ ) { return "[Alarm] Invalid argument to set $cmd, must be numeric" if ( $args[0] !~ /\d+/ ); return "[Alarm] Invalid argument to set $cmd, must be 0 < arg < $alarmno" if ( ($args[0] >= $alarmno)||($args[0]<0) ); if( $cmd =~ /^cancel(ed)?$/ ){ Alarm_Exec($name,$args[0],"web","button","cancel"); }elsif ( $cmd =~ /^arm(ed)?$/ ) { Alarm_Arm($name,$args[0],"web","button","arm"); }elsif ( $cmd =~ /^disarm(ed)?$/ ){ Alarm_Arm($name,$args[0],"web","button","disarm"); }else{ return "[Alarm] Invalid argument set $cmd"; } return; #----------------------------------------------------------- } elsif ( $cmd =~ /^lock(ed)?$/ ) { readingsSingleUpdate( $hash, "lockstate", "locked", 0 ); return; #----------------------------------------------------------- } elsif ( $cmd =~ /^unlock(ed)?$/ ) { readingsSingleUpdate( $hash, "lockstate", "unlocked", 0 ); return; #----------------------------------------------------------- } elsif ( $cmd =~ /^save/ ) { return Alarm_save($hash); #----------------------------------------------------------- } elsif ( $cmd =~ /^restore/ ) { return Alarm_restore($hash,1); } else { my $str = join(",",(0..($alarmno-1))); return "[Alarm] Unknown argument " . $cmd . ", choose one of canceled:$str armed:$str disarmed:$str locked:noArg unlocked:noArg save:noArg restore:noArg"; } } ######################################################################################### # # Alarm_Get - Implements the Get function # # Parameter hash = hash of device addressed # ######################################################################################### sub Alarm_Get($@) { my ($hash, @a) = @_; my $res = ""; my $arg = (defined($a[1]) ? $a[1] : ""); if ($arg eq "version") { return "Alarm.version => $alarmversion"; } else { return "Unknown argument $arg choose one of version:noArg"; } } ######################################################################################### # # Alarm_getsettings - Helper function to assemble the alarm settings for a device # # Parameter hash = hash of Alarm device # dev = name of device addressed # ######################################################################################### sub Alarm_getsettings($$$){ my ($hash,$dev,$type) = @_; my $chg = 0; my $avl = AttrVal($dev, "alarmSettings","|||0:00"); $avl = Alarm_escape($avl,"beforesplit"); my @aval = split('\|',$avl,4); $aval[1] = Alarm_escape($aval[1],"aftersplit"); $aval[2] = Alarm_escape($aval[2],"aftersplit"); if( $type eq "Actor"){ #-- position 0:set by level, 1:set func, 2:unset func, 3:delay if( $aval[0] eq "" || $aval[1] eq "" ){ Log3 $hash, 1, "[Alarm] Settings $avl incomplete for alarmActor $dev"; } #-- check if empty unset action if( !defined($aval[3]) ){ $aval[3] = $aval[2]; $aval[2] = ""; } #-- check delay time if( $aval[3] =~ /^\d+$/ ){ if( $aval[3] > 3559 ){ Log3 $hash, 1, "[Alarm] Delay time $aval[3] for alarmActor $dev to large as single number, maximum 3559 seconds";; $aval[3] = "59:59"; }else{ my $min = int($aval[3]/60); my $sec = $aval[3]%60; $aval[3] = sprintf("%02d:%02d",$min,$sec); } $chg = 1; }elsif( $aval[3] !~ /^(\d\d:)?\d?\d:\d\d/ ){ Log3 $hash, 1, "[Alarm] Delay time $aval[3] ill defined for alarmActor $dev"; $aval[3] = "0:00"; $chg = 1; } if( $chg==1 ){ CommandAttr(undef,$dev.' alarmSettings '.join('|',@aval)); } } return @aval; } ######################################################################################### # # Alarm_escape - Helper function to de-escape and to escape action parameters # # Parameter hash = hash of Alarm device # dev = name of device addressed # ######################################################################################### sub Alarm_escape($$){ my ($str,$type) = @_; if( $type eq "beforesplit"){ $str =~ s/\\\|/%7C/g; }elsif( $type eq "aftersplit"){ $str =~ s/\\//g; $str =~ s/%7C/\|/g; }elsif( $type eq "beforehtml"){ $str =~ s/\\//g; $str =~ s/\|/\\\|/g; } return $str; } ######################################################################################### # # Alarm_save # # Parameter hash = hash of the Alarm device # ######################################################################################### sub Alarm_save($) { my ($hash) = @_; my $date = TimeNow(); $hash->{DATA}{"savedate"} = $date; readingsSingleUpdate( $hash, "savedate", $date, 1 ); my $jhash0 = toJSON($hash->{DATA}); my $error = FileWrite("AlarmFILE",$jhash0); #Log 1,"[Alarm_save] error=$error"; return; } ######################################################################################### # # Alarm_restore # # Parameter hash = hash of the Alarm device # ######################################################################################### sub Alarm_restore($$) { my ($hash,$doit) = @_; my $name = $hash->{NAME}; my ($error,$jhash0) = FileRead("AlarmFILE"); if( defined($error) && $error ne "" ){ Log3 $name,1,"[Alarm_restore] read error=$error"; return undef; } my $json = JSON->new->utf8; my $jhash1 = eval{ $json->decode( $jhash0 ) }; my $date = $jhash1->{"savedate"}; #-- just for the first time, reading an old savefile $date = localtime(time) if( !defined($date)); readingsSingleUpdate( $hash, "savedate", $date, 0 ); if( $doit==1 ){ $hash->{DATA} = {%{$jhash1}}; Log3 $name,5,"[Alarm_restore] Data hash restored from save file with date ".$date; return 1; }else{ return $date; } } ######################################################################################### # # Alarm_Test - Test an actor # # Parameter name = name of the Alarm definition # cmd = # ######################################################################################### sub Alarm_Test($$){ my ($name,$cmd) = @_; my $hash = $defs{$name}; $cmd =~ s/\$NAME/Gerät/g; $cmd =~ s/\$EVENT/Event/g; $cmd =~ s/\$SHORT/Kurznachricht/g; #for( my $i=1;$i<= int(@evtpart);$i++){ # $cmd =~ s/\$EVTPART$i/$evtpart[$i-1]/g; #} fhem($cmd); } ######################################################################################### # # Alarm_Exec - Execute the Alarm # # Parameter name = name of the Alarm definition # level = Alarm level # dev = name of the device calling the alarm # evt = event calling the alarm # act = action - "on" or "off" # ######################################################################################### sub Alarm_Exec($$$$$){ my ($name,$level,$dev,$evt,$act) = @_; my $hash = $defs{$name}; my $xec = $hash->{DATA}{"armstate"}{"level".$level}; my $xac = $hash->{READINGS}{"level".$level}{VAL}; my $msg = ''; my $cmd; my $mga; my $dly; my @sta; #Log3 $hash,1,"[Alarm $level] Exec called with dev $dev evt $evt act $act]"; return if ($dev eq 'global'); return if (!defined($level)); #-- raising the alarm if( $act eq "on" ){ #-- only if this level is armed and not yet active if( ($xec eq "armed") && ($xac eq "armed") ){ #-- check for condition my $cond = AttrVal($name, "level".$level."cond", 1); $cond =~ s/\$NAME/$dev/g; if( eval($cond) ne "1" ){ Log3 $hash,1,"[Alarm $level] Cannot be executed due to condition {$cond} not equal to 1 for level".$level."cond"; return; } #-- check for time my $start = AttrVal($name, "level".$level."start", "0:00"); if( index($start, '{') != -1){ $start = eval($start); } my @st = split(':',$start); if( (int(@st)>3) || (int(@st)<2) || ($st[0] > 23) || ($st[0] < 0) || ($st[1] > 59) || ($st[1] < 0) ){ Log3 $hash,1,"[Alarm $level] Cannot be executed due to wrong time spec $start for level".$level."start"; return; } my $end = AttrVal($name, "level".$level."end", "23:59"); if( index($end, '{') != -1){ $end = eval($end); } my @et = split(':',$end); if( (int(@et)>3) || (int(@et)<2) || ($et[0] > 23) || ($et[0] < 0) || ($et[1] > 59) || ($et[1] < 0) ){ Log3 $hash,1,"[Alarm $level] Cannot be executed due to wrong time spec $end for level".$level."end"; return; } my $dur = AttrVal($name, "level".$level."autocan", "0:00"); if( index($dur, '{') != -1){ $dur = eval($dur); } my @dt = split(':',$dur); if( (int(@dt)>3) || (int(@dt)<2) || ($dt[0] > 23) || ($dt[0] < 0) || ($dt[1] > 59) || ($dt[1] < 0) ){ Log3 $hash,1,"[Alarm $level] Cannot be executed due to wrong time spec $dur for level".$level."autocan"; return; } my $stp = $st[0]*60+$st[1]; my $etp = $et[0]*60+$et[1]; my $dtp = $dt[0]*60+$dt[1]; my ($sec, $min, $hour, $day, $month, $year, $wday, $yday, $isdst) = localtime(time); my $ntp = $hour*60+$min; if( (($stp < $etp) && ($ntp <= $etp) && ($ntp >= $stp)) || (($stp > $etp) && (($ntp <= $etp) || ($ntp >= $stp))) ){ #-- raised by sensor (attribute values have been controlled in CreateNotifiers) my $avl = AttrVal($dev, "alarmSettings",""); $avl =~ s/\\\|/%7C/g; @sta = split('\|',$avl); for( my $i=0;$i<4;$i++ ){ $sta[$i] =~ s/\\//g; $sta[$i] =~ s/%7C/|/g; } if( $sta[2] ){ $mga = $sta[2]." ".AttrVal($name, "level".$level."msg", 0); #-- replace some parts my @evtpart = split(" ",$evt); $mga =~ s/\$NAME/$dev/g; $mga =~ s/\$EVENT/$evt/g; for( my $i=1;$i<= int(@evtpart);$i++){ $mga =~ s/\$EVTPART$i/$evtpart[$i-1]/g; } #-- readings readingsSingleUpdate( $hash, "level".$level,$dev,1 ); readingsSingleUpdate( $hash, "short", $mga, 1); $msg = Alarm_getstate($hash)." ".$mga; readingsSingleUpdate( $hash, "state", $msg, 1 ); $msg = "[Alarm $level] raised from device $dev with event $evt"; #-- set up timer for auto cancel if( $dur ne "0:00" ){ my $cmd = "alarm".$level.".autocancel at +$dur {Alarm_Exec('$name',$level,'".$alarm_tt->{"autocan"}."','dummy','off')}"; CommandDefine(undef,$cmd); CommandAttr (undef,'alarm'.$level.'.autocancel room '.$alarmpublicroom); CommandAttr (undef,'alarm'.$level.'.autocancel group alarmHelper'); } #-- calling actors AFTER state update $cmd = AttrVal($name, "level".$level."onact", 0); $cmd =~ s/\$NAME/$dev/g; $cmd =~ s/\$EVENT/$evt/g; $cmd =~ s/\$SHORT/$mga/g; for( my $i=1;$i<= int(@evtpart);$i++){ $cmd =~ s/\$EVTPART$i/$evtpart[$i-1]/g; } fhem($cmd); Log3 $hash,3,$msg; }else{ $msg = "[Alarm $level] not raised, alarmSensor $dev has wrong settings"; Log3 $hash,1,$msg; } }else{ $msg = "[Alarm $level] not raised, not in time slot"; Log3 $hash,5,$msg; } }else{ $msg = "[Alarm $level] not raised, not armed or already active"; Log3 $hash,5,$msg; } }elsif( ($act eq "off")||($act eq "cancel") ){ #-- only if this level is active if( ($xac ne "armed")&&($xac ne "disarmed") ){ #-- TODO: ohne intAt auskommen #-- deleting all running ats CommandDelete(undef,'alarm'.$level.'.autocancel'); $dly = sprintf("alarm%1ddly",$level); foreach my $d ( devspec2array("NAME=alarm.dly.*")) { Log3 $hash,1,"[Alarm] Killing delayed action $d"; CommandDelete(undef,$d); } #-- replace some parts my @evtpart = split(" ",$evt); #-- calling actors BEFORE state update $cmd = AttrVal($name, "level".$level."offact", 0); $cmd =~ s/\$NAME/$dev/g; $cmd =~ s/\$EVENT/$evt/g; $cmd =~ s/\$SHORT/$mga/g; for( my $i=1;$i<= int(@evtpart);$i++){ $cmd =~ s/\$EVTPART$i/$evtpart[$i-1]/g; } fhem($cmd); $cmd = AttrVal($name, "cancelact", 0); fhem($cmd) if( $cmd ); #-- readings - arm status does not change readingsSingleUpdate( $hash, "level".$level,"canceled",1); readingsSingleUpdate( $hash, "level".$level,"armed",1); readingsSingleUpdate( $hash, "short", "", 0); $mga = Alarm_getstate($hash)." ".$alarm_tt->{"canceled"}." ".$dev; readingsSingleUpdate( $hash, "state", $mga, 1 ); $msg = "[Alarm $level] canceled from device $dev"; Log3 $hash,3,$msg; } }else{ Log3 $hash,3,"[Alarm $level] Exec called with act=$act"; } #return $msg; } ######################################################################################### # # Alarm_Arm - Arm the Alarm # # Parameter name = name of the Alarm definition # level = Alarm level # dev = name of the device calling the alarm # evt = Event of the device # act = action - "armed" or "disarmed" # ######################################################################################### sub Alarm_Arm($$$$$){ my ($name,$level,$dev,$evt,$act) = @_; my $hash = $defs{$name}; my $xec = $hash->{DATA}{"armstate"}{"level".$level}; my $xac = $hash->{READINGS}{"level".$level}{VAL}; my $msg = ''; my $mga; my $cmd; #-- arming the alarm if( ($act eq "arm") && ( $xac ne "armed") ){ my $xdl = AttrVal($name, "armdelay", 0); my $cmdwait = AttrVal($name, "armwait", 0); #-- immediate arming if( ($xdl eq '')||($xdl eq '0:00')||($xdl eq '00:00')||($evt eq "delay") ){ my $cmdact = AttrVal($name, "armact", 0); #-- update state display $hash->{DATA}{"armstate"}{"level".$level} = "armed"; readingsSingleUpdate( $hash, "level".$level,"armed",1 ); readingsSingleUpdate( $hash, "state", Alarm_getstate($hash)." ".$hash->{READINGS}{"short"}{VAL}, 1 ); #-- save new state Alarm_save($hash); #--transform commands from fhem to perl level my @cmdactarr = split(/;/,$cmdact); my $cmdactf; if( int(@cmdactarr) == 1 ){ fhem("$cmdact"); }else{ for(my $i=0;$i{DATA}{"armstate"}{"level".$level} = "armed"; readingsSingleUpdate( $hash, "level".$level,"armwait",1 ); readingsSingleUpdate( $hash, "state", Alarm_getstate($hash)." ".$hash->{READINGS}{"short"}{VAL}, 1 ); #-- save new state Alarm_save($hash); #-- compose commands TODO $cmd = sprintf("defmod alarm%1d.arm.dly at +00:%02d:%02d {Alarm_Arm(\"%s\",%1d,\"%s\",\"delay\",\"arm\")}", $level,$1,$2,$name,$level,$dev); $msg = "[Alarm $level] will be armed from alarmSensor $dev with event $evt, delay $xdl"; #-- define new delayed arm fhem($cmd); #-- execute armwait action fhem($cmdwait); Log3 $hash,1,$msg; }else{ $msg = "[Alarm $level] cannot be armed due to wrong delay timespec"; Log3 $hash,1,$msg; } #-- disarming implies canceling as well }elsif( ($act eq "disarm") && ($xec ne "disarmed")) { #-- delete stale delayed arm if( defined $defs{'alarm'.$level.'.arm.dly'}){ fhem('delete alarm'.$level.'.arm.dly' ) #-- really kill active alarm }else{ Alarm_Exec($name,$level,"program","disarm","cancel"); } $hash->{DATA}{"armstate"}{"level".$level} = "disarmed"; #-- update state display readingsSingleUpdate( $hash, "level".$level,"disarmed",1 ); readingsSingleUpdate( $hash, "state", Alarm_getstate($hash)." ".$hash->{READINGS}{"short"}{VAL}, 1 ); #-- save new state Alarm_save($hash); #-- $msg = "[Alarm $level] disarmed from alarmSensor $dev with event $evt"; $cmd = AttrVal($name, "disarmact", 0); fhem("define alarm".$level.".disarm.T at +00:00:03 ".$cmd) if( $cmd ); } return $msg; } ######################################################################################### # # Alarm_CreateNotifiers - Create the notifiers # # Parameter name = name of the Alarm definition # ######################################################################################### sub Alarm_CreateNotifiers($){ my ($name) = @_; my $ret = ""; my $res; my $hash = $defs{$name}; #-- don't do anything if locked if( $hash->{READINGS}{"lockstate"}{VAL} ne "unlocked" ){ Log3 $hash, 1, "[Alarm] State locked, cannot create new notifiers"; return "State locked, cannot create new notifiers"; } for( my $level=0;$level<$alarmno;$level++ ){ #-- delete old defs in any case fhem('delete alarm'.$level.'.on.N' ) if( defined $defs{'alarm'.$level.'.on.N'}); fhem('delete alarm'.$level.'.off.N' ) if( defined $defs{'alarm'.$level.'.off.N'}); fhem('delete alarm'.$level.'.arm.N' ) if( defined $defs{'alarm'.$level.'.arm.N'}); fhem('delete alarm'.$level.'.disarm.N' ) if( defined $defs{'alarm'.$level.'.disarm.N'}); my $start = AttrVal($name, "level".$level."start", 0); my @st; if( index($start,'{')!=-1 ){ Log3 $hash,1,"[Alarm $level] perl function $start detected for level".$level."start, currently the function gives ".eval($start); }else{ @st = split(':',($start ne '') ? $start :'0:00'); if( (int(@st)!=2) || ($st[0] > 23) || ($st[0] < 0) || ($st[1] > 59) || ($st[1] < 0) ){ Log3 $hash,1,"[Alarm $level] Will not be executed due to wrong time spec $start for level".$level."start"; next; } } my $end = AttrVal($name, "level".$level."end", 0); my @et; if( index($end,'{')!=-1 ){ Log3 $hash,1,"[Alarm $level] perl function $end detected for level".$level."end, currently the function gives ".eval($end); }else{ @et = split(':',($end ne '') ? $end :'23:59'); if( (int(@et)!=2) || ($et[0] > 23) || ($et[0] < 0) || ($et[1] > 59) || ($et[1] < 0) ){ Log3 $hash,1,"[Alarm $level] Will not be executed due to wrong time spec $end for level".$level."end"; next; } } #-- now set up the command for cancel alarm, and contained in this loop all other notifiers as well my $cmd = ''; foreach my $d (keys %defs ) { next if(IsIgnored($d)); if( AttrVal($d, "alarmDevice","") eq "Sensor" ) { my $avl = AttrVal($d, "alarmSettings",""); $avl =~ s/\\\|/%7C/g; my @aval = split('\|',$avl); if( int(@aval) != 4){ Log3 $hash,1, "[Alarm $level] Settings $avl incomplete for alarmSensor $d"; next; } for( my $i=0;$i<4;$i++ ){ $aval[$i] =~ s/\\//g; $aval[$i] =~ s/%7C/\|/g; } #-- workaround: replace any space by \s $aval[1] =~ s/\s/\\s/g; if( (index($aval[0],"alarm".$level) != -1) && ($aval[1] ne "") && ($aval[3] eq "off")){ $cmd .= '('.$aval[1].')|'; Log3 $hash,5,"[Alarm $level] Adding sensor $d to cancel notifier"; } } } if( $cmd eq '' ){ Log3 $hash,1,"[Alarm $level] No \"Cancel\" device defined, level will be ignored"; } else { $cmd = substr($cmd,0,length($cmd)-1); $cmd = 'alarm'.$level.'.off.N notify '.$cmd; $cmd .= ' {main::Alarm_Exec("'.$name.'",'.$level.',"$NAME","$EVENT","off")}'; CommandDefine(undef,$cmd); CommandAttr (undef,'alarm'.$level.'.off.N room '.$alarmpublicroom); CommandAttr (undef,'alarm'.$level.'.off.N group alarmNotifier'); Log3 $hash,3,"[Alarm $level] Created cancel notifier"; #-- now set up the command for raising alarm - only if cancel exists $cmd = ''; my $cmdarm = ""; my $cmddisarm = ""; foreach my $d (sort keys %defs ) { next if(IsIgnored($d)); if( AttrVal($d, "alarmDevice","") eq "Sensor" ) { my $avl = AttrVal($d, "alarmSettings",""); my @aval = split('\|',$avl); if( int(@aval) != 4){ Log3 $hash, 1, "[Alarm $level] Settings $avl incomplete for alarmSensor $d"; next; } if( index($aval[0],"alarm".$level) != -1){ if( ($aval[1] ne "") && ($aval[3] eq "on") ){ $cmd .= '('.$aval[1].')|'; Log3 $hash,5,"[Alarm $level] Adding alarmSensor $d to raise notifier"; }elsif( ($aval[1] ne "") && ($aval[3] eq "arm") ){ $cmdarm .= '('.$aval[1].')|'; Log3 $hash,5,"[Alarm $level] Adding alarmSensor $d to arm notifier"; }elsif( ($aval[1] ne "") && ($aval[3] eq "disarm") ){ $cmddisarm .= '('.$aval[1].')|'; Log3 $hash,5,"[Alarm $level] Adding alarmSensor $d to disarm notifier"; } } } } #-- raise notifier if( $cmd eq '' ){ CommandAttr(undef,$name.' level'.$level.'onact 1'); CommandAttr(undef,$name.' level'.$level.'offact 1'); Log3 $hash,1,"[Alarm $level] No \"Raise\" device defined"; } else { $cmd = substr($cmd,0,length($cmd)-1); $cmd = 'alarm'.$level.'.on.N notify '.$cmd; $cmd .= ' {main::Alarm_Exec("'.$name.'",'.$level.',"$NAME","$EVENT","on")}'; CommandDefine(undef,$cmd); CommandAttr (undef,'alarm'.$level.'.on.N room '.$alarmpublicroom); CommandAttr (undef,'alarm'.$level.'.on.N group alarmNotifier'); Log3 $hash,3,"[Alarm $level] Created raise notifier"; #-- now set up the list of actors $cmd = ''; my $cmd2 = ''; my $nonum = 0; foreach my $d (sort keys %defs ) { next if(IsIgnored($d)); if( AttrVal($d, "alarmDevice","") eq "Actor" ) { my @aval = Alarm_getsettings($hash,$d,"Actor"); if( int(@aval) != 4){ Log3 $hash, 3, "[Alarm $level] Settings incomplete for alarmActor $d"; next; } if( index($aval[0],"alarm".$level) != -1 ){ #-- activate without delay if(( $aval[3] eq "" )||($aval[3] =~ /(00:)?0?0:00/)){ $cmd .= $aval[1].';'; #-- activate with delay } else { $nonum++; my @tarr = split(':',$aval[3]); if( int(@tarr) == 2){ $cmd .= sprintf('defmod alarm%1ddly%1d at +00:%02d:%02d %s;',$level,$nonum,$tarr[0],$tarr[1],$aval[1]); }elsif( int(@tarr) == 3){ $cmd .= sprintf('defmod alarm%1ddly%1d at +%02d:%02d:%02d %s;',$level,$nonum,$tarr[0],$tarr[1],$tarr[2],$aval[1]); }else{ Log3 $name,1,"[Alarm $level] Invalid delay specification for actor $d, skipped"; $cmd .= $aval[1].';'; } } $cmd2 .= $aval[2].';' if( $aval[2] ne '' ); Log3 $hash,5,"[Alarm $level] Adding actor $d to action list"; } } } if( $cmd ne '' ){ CommandAttr(undef,$name.' level'.$level.'onact '.$cmd); CommandAttr(undef,$name.' level'.$level.'offact '.$cmd2); Log3 $hash,5,"[Alarm $level] Added on/off actors to $name"; } else { CommandAttr(undef,$name.' level'.$level.'onact 1'); CommandAttr(undef,$name.' level'.$level.'offact 1'); Log3 $hash,5,"[Alarm $level] Adding on/off actors not possible"; } #-- arm notifier - optional, but only in case the alarm may be raised if( $cmdarm ne '' ){ $cmdarm = substr($cmdarm,0,length($cmdarm)-1); $cmdarm = 'alarm'.$level.'.arm.N notify '.$cmdarm; $cmdarm .= ' {main::Alarm_Arm("'.$name.'",'.$level.',"$NAME","$EVENT","arm")}'; CommandDefine(undef,$cmdarm); CommandAttr (undef,'alarm'.$level.'.arm.N room '.$alarmpublicroom); CommandAttr (undef,'alarm'.$level.'.arm.N group alarmNotifier'); Log3 $hash,3,"[Alarm $level] Created arm notifier"; } #-- disarm notifier - optional, but only in case the alarm may be raised if( $cmddisarm ne '' ){ $cmddisarm = substr($cmddisarm,0,length($cmddisarm)-1); $cmddisarm = 'alarm'.$level.'.disarm.N notify '.$cmddisarm; $cmddisarm .= ' {main::Alarm_Arm("'.$name.'",'.$level.',"$NAME","$EVENT","disarm")}'; CommandDefine(undef,$cmddisarm); CommandAttr (undef,'alarm'.$level.'.disarm.N room '.$alarmpublicroom); CommandAttr (undef,'alarm'.$level.'.disarm.N group alarmNotifier'); Log3 $hash,3,"[Alarm $level] Created disarm notifier"; } } } } return "Created alarm notifiers"; } ######################################################################################### # # Alarm_getstate - Helper function to assemble a state display # # Parameter hash = hash of device addressed # ######################################################################################### sub Alarm_getstate($) { my ($hash) = @_; my $name = $hash->{NAME}; my $res = ''; my $type = AttrVal($hash->{NAME},"statedisplay",0); my $val; #-------------------------- if( $type eq "simple" ){ for( my $level=0;$level<$alarmno;$level++ ){ $val = $hash->{READINGS}{"level".$level}{VAL}; if( $val eq "disarmed" ){ $res .= '-'; }elsif( $val eq "armwait" ){ $res .= 'o'; }elsif( $val eq "armed" ){ $res .= 'O'; }else{ $res .= 'X'; } } #-------------------------- }else{ my $dac = AttrVal($name,"disarmcolor","lightgray"); my $ac = AttrVal($name,"armcolor","#53f3c7"); my $awc = AttrVal($name,"armwaitcolor","#ffe658"); my $alc = AttrVal($name,"alarmcolor","#fd5777"); if( $type eq "color" ){ $res = ''; for( my $level=0;$level<$alarmno;$level++ ){ $val = $hash->{READINGS}{"level".$level}{VAL}; if( $val eq "disarmed" ){ $res .= ' '.$level; }elsif( $val eq "armwait" ){ $res .= ' '.$level.''; }elsif( $val eq "armed" ){ $res .= ' '.$level.''; }else{ $res .= ' '.$level.''; } } $res.=''; #-------------------------- }elsif( $type eq "table" ){ $res = ''; for( my $level=0;$level<$alarmno;$level++ ){ $val = $hash->{READINGS}{"level".$level}{VAL}; if( $val eq "disarmed" ){ $res .= '
'; }elsif( $val eq "armwait" ){ $res .= ''; }elsif( $val eq "armed" ){ $res .= ''; }else{ $res .= ''; } } $res.='
'; } } return $res; } ######################################################################################### # # Alarm_widget - returns animated SVG-code for the Alarm page # # Parameter name = name of the Alarm definition # ######################################################################################### sub Alarm_widget($){ my ($arg) = @_; my $name = $FW_webArgs{name}; my $sizep = $FW_webArgs{size}; my $gstate = ( $FW_webArgs{gstate} ? $FW_webArgs{gstate} : "disarmed"); my $dstate = ( $FW_webArgs{dstate} ? $FW_webArgs{dstate} : "--------"); my $inline = 0; #-- no webarg, check direct parameter. TODO: order if( !defined($name) || $name eq "" ){ if( $arg =~ /^name=(\w*)&gstate=(.+)&dstate=(.*)&size=(\d+x\d+)/ ){ $name = $1; $gstate = $2; $dstate = $3; $sizep = $4; $inline = 1; } } Log 5,"[Alarm_widget] name=$name gstate=$gstate dstate=$dstate sizep=$sizep"; $name =~ s/'//g; my @size=split('x',($sizep ? $sizep : '60x80')); my ($fillcolor,$fillcolor2); my $dac = AttrVal($name,"disarmcolor","lightgray"); my $ac = AttrVal($name,"armcolor","#53f3c7"); my $awc = AttrVal($name,"armwaitcolor","#ffe658"); my $alc = AttrVal($name,"alarmcolor","#fd5777"); if($gstate eq "disarmed"){ $fillcolor = AttrVal($name,"disarmcolor",$dac); $fillcolor2 = "white"; }elsif($gstate eq "armed"){ $fillcolor = AttrVal($name,"armcolor",$ac); $fillcolor2 = "white"; }elsif($gstate eq "mixed"){ $fillcolor = AttrVal($name,"armwaitcolor",$awc); $fillcolor2 = "white"; }else{ $fillcolor = AttrVal($name,"alarmcolor",$alc); $fillcolor2 = $fillcolor; } my $hash = $defs{$name}; my $id = $defs{$name}{NR}; my $ret=""; $ret = ""; $ret .= "". ""; $ret .= ""; $ret .= ""; $ret .= ""; for( my $level=0;$level<$alarmno;$level++ ){ my $val = $hash->{READINGS}{"level".$level}{VAL}; my $col; if($val eq "disarmed"){ $col = $dac; }elsif($val eq "armed"){ $col = $ac; }elsif($val eq "armwait"){ $col = $awc; }else{ $col = $alc; } $ret .= ""; } $ret .= ""; return $ret; if( $inline ){ return $ret; }else{ $FW_RETTYPE = "image/svg+xml"; $FW_RET=""; FW_pO $ret; return ($FW_RETTYPE, $FW_RET); } } ######################################################################################### # # Alarm_Html - returns HTML code for the Alarm page # # Parameter name = name of the Alarm definition # ######################################################################################### sub Alarm_Html($){ my ($name) = @_; my $ret = ""; my $hash = $defs{$name}; my $id = $defs{$name}{NR}; if( !defined($alarm_tt) ){ #-- readjust language my $lang = AttrVal("global","language","EN"); if( $lang eq "DE"){ $alarm_tt = \%alarm_transtable_DE; }else{ $alarm_tt = \%alarm_transtable_EN; } } #-- update state display readingsSingleUpdate( $hash, "state", Alarm_getstate($hash)." ".$hash->{READINGS}{"short"}{VAL}, 1 ); #-- my $lockstate = ($hash->{READINGS}{lockstate}{VAL}) ? $hash->{READINGS}{lockstate}{VAL} : "unlocked"; my $showhelper = ($lockstate eq "unlocked") ? 1 : 0; #-- $ret .= "\n"; $ret .= "\n"; #--- here we insert the icon $ret .= ""; $ret .= "\n"; #-- settings table my $row=1; $ret .= ""; $ret .= "
"; $ret .= "
".Alarm_widget("name=".$name."&gstate=".$iconstate."&dstate=".$detailstate."&size=60x80")."
" if( AttrVal($name,"noicons",0)==0 ); $ret .= "
{"setparms"}."\" onclick=\"javascript:alarm_set('$name')\"/>". "
"; for( my $k=0;$k<$alarmno;$k++ ){ $ret .= "
".$hash->{READINGS}{"level".$k}{VAL}."
"; } $ret .= "
".$alarm_tt->{"settings"}."
\n"; $ret .= ""; $ret .= ""; #-- sensors table $row=1; $ret .= ""; $ret .= ""; #-- actors table $row=1; $ret .= ""; $ret .= "\n"; $ret .= "
\n"; $ret .= ""; $ret .= "\n"; $ret .= "\n"; $ret .="
".$alarm_tt->{"armbutton"}." ↠ ".$alarm_tt->{"waitaction"}." "; $ret .= sprintf("",(AttrVal($name, "armwait","") eq "1")?"":AttrVal($name, "armwait","")); $ret .= " ↴ ".$alarm_tt->{"delay"}."
↲"; $ret .= sprintf("",(AttrVal($name, "armdelay","0:00") eq "1")?"":AttrVal($name, "armdelay","0:00")); $ret .= "
".$alarm_tt->{"armaction"}." "; $ret .= sprintf("",(AttrVal($name, "armact","") eq "1")?"":AttrVal($name, "armact","")); $ret .= "
".$alarm_tt->{"disarmbutton"}."↠".$alarm_tt->{"disarmaction"}." "; $ret .= sprintf("",(AttrVal($name, "disarmact","") eq "1")?"":AttrVal($name, "disarmact","")); $ret .= "
".$alarm_tt->{"cancelbutton"}." ↠ ".$alarm_tt->{"cancelaction"}." "; $ret .= sprintf("",(AttrVal($name, "cancelact","") eq "1")?"":AttrVal($name, "cancelact","")); $ret .= "
". "". "\n"; #-- foreach level for( my $k=0;$k<$alarmno;$k++ ){ $row++; my $cval = AttrVal($name, "level".$k."cond", 1); my $sval = AttrVal($name, "level".$k."start", "0:00"); $sval = "" if( $sval eq "1"); my $eval = AttrVal($name, "level".$k."end", "23:59"); $eval = "" if( $eval eq "1"); my $oval = AttrVal($name, "level".$k."autocan", "0:00"); $oval = "" if( $oval eq "1"); my $mval = AttrVal($name, "level".$k."msg", "--"); $mval = "" if( $mval eq "1"); my $xval = $hash->{DATA}{"armstate"}{"level".$k}; $ret .= sprintf("\n", ($row&1)?"odd":"even"); $ret .= "". ""; $ret .= sprintf("\n"; } $ret .= "
".$alarm_tt->{"time"}." [hh:mm]
".$alarm_tt->{"level"}."".$alarm_tt->{"cond"}. "".$alarm_tt->{"start"}."".$alarm_tt->{"end"}. "".$alarm_tt->{"autocan"}. "".$alarm_tt->{"messagepart"}." II".$alarm_tt->{"arm"}."".$alarm_tt->{"cancel"}."
".$alarm_tt->{"alarm"}." $k". "". "". "",($xval eq "armed")?"checked=\"checked\"":""). "{"cancel"}."\" onclick=\"javascript:alarm_cancel('$name','$k')\"/>
".$alarm_tt->{"sensors"}."
\n"; $ret .= "\n"; foreach my $d (sort keys %defs ) { next if(IsIgnored($d)); if( AttrVal($d, "alarmDevice","") eq "Sensor" ) { my $avl = AttrVal($d, "alarmSettings",""); #-- no escaping necessary my @aval = split('\|',$avl); if( int(@aval) != 4){ Log3 $hash, 1, "[Alarm] Settings incomplete for alarmSensor $d"; } $row++; $ret .= sprintf("", ($row&1)?"odd":"even"); $ret .= "\n"; $ret .= "\n"; $ret .= sprintf("\n", ($aval[3] eq "arm")?"selected=\"seleced\"":"",($aval[3] eq "disarm")?"selected=\"selected\"":""); } } $ret .= "
".$alarm_tt->{"notifyto"}." ".$alarm_tt->{"alarm"}." ".$alarm_tt->{"level"}."
". join("     ",(0..($alarmno-1)))."
". $alarm_tt->{"notifyby"}." ".$alarm_tt->{"regexp"}."".$alarm_tt->{"messagepart"}." I".$alarm_tt->{"action"}."
$d\n"; for( my $k=0;$k<$alarmno;$k++ ){ $ret .= sprintf(" ",(index($aval[0],"alarm".$k) != -1)?"checked=\"checked\"":""); } $ret .= ""; $ret .= "
".$alarm_tt->{"actors"}."
\n"; $ret .= "\n"; foreach my $d (sort keys %defs ) { next if(IsIgnored($d)); if( AttrVal($d, "alarmDevice","") eq "Actor" ) { my @aval = Alarm_getsettings($hash,$d,"Actor"); #-- escaping before HTML publish $aval[1] = Alarm_escape($aval[1],"beforehtml"); $aval[2] = Alarm_escape($aval[2],"beforehtml"); $aval[3] = Alarm_escape($aval[3],"beforehtml"); $row++; $ret .= sprintf("", ($row&1)?"odd":"even"); $ret .= "\n"; $ret .= "\n"; } } $ret .= "
".$alarm_tt->{"setby"}." ".$alarm_tt->{"alarm"}." ".$alarm_tt->{"level"}."
".join("     ",(0..($alarmno-1))). "
".$alarm_tt->{"setaction"}; $ret .= "  (".$alarm_tt->{"testaction"}.")" if( AttrVal($name,"testbutton",0) == 1); $ret .= "".$alarm_tt->{"unsetaction"}; $ret .= "  (".$alarm_tt->{"testaction"}.")" if( AttrVal($name,"testbutton",0) == 1); $ret .= "".$alarm_tt->{"delay"}."
[hh:]mm:ss
$d\n"; for( my $k=0;$k<$alarmno;$k++ ){ $ret .= sprintf(" ",(index($aval[0],"alarm".$k) != -1)?"checked=\"checked\"":""); } $ret .= ""; $ret .= " " if( AttrVal($name,"testbutton",0) == 1); $ret .= ""; $ret .= ""; $ret .= " " if( AttrVal($name,"testbutton",0) == 1); $ret .= "
"; return $ret; } 1; =pod =item helper =item summary to set up a house alarm system with 8 different alarm levels =begin html

Alarm

=end html =begin html_DE

Alarm

=end html_DE =cut