#!/usr/bin/perl ################################################################ # # Copyright notice # # (c) 2005-2012 # Copyright: Rudolf Koenig (r dot koenig at koeniglich dot de) # All rights reserved # # This script 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. # # This copyright notice MUST APPEAR in all copies of the script! # Thanks for Tosti's site () # for inspiration. # # Homepage: http://fhem.de # # $Id$ use strict; use warnings; use IO::Socket; use Time::HiRes qw(gettimeofday); ################################################## # Forward declarations # sub AddDuplicate($$); sub AnalyzeCommand($$); sub AnalyzeCommandChain($$); sub AnalyzePerlCommand($$); sub AnalyzeInput($); sub AssignIoPort($); sub AttrVal($$$); sub addToAttrList($); sub CallFn(@); sub CommandChain($$); sub CheckDuplicate($$); sub DoTrigger($$); sub Dispatch($$$); sub EventMapAsList($); sub FmtDateTime($); sub FmtTime($); sub GetLogLevel(@); sub GetTimeSpec($); sub HandleArchiving($); sub HandleTimeout(); sub IOWrite($@); sub InternalTimer($$$$); sub LoadModule($); sub Log($$); sub OpenLogfile($); sub PrintHash($$); sub ReadingsVal($$$); sub ReplaceEventMap($$$); sub ResolveDateWildcards($@); sub RemoveInternalTimer($); sub SecondsTillTomorrow($); sub SemicolonEscape($); sub SignalHandling(); sub TimeNow(); sub WriteStatefile(); sub XmlEscape($); sub devspec2array($); sub doGlobalDef($); sub fhem($); sub fhz($); sub getAllSets($); sub IsDummy($); sub IsIgnored($); sub setGlobalAttrBeforeFork($); sub redirectStdinStdErr(); sub setReadingsVal($$$$); sub addEvent($$); sub createInterfaceDefinitions(); sub CommandAttr($$); sub CommandDefaultAttr($$); sub CommandDefine($$); sub CommandDeleteAttr($$); sub CommandDelete($$); sub CommandGet($$); sub CommandHelp($$); sub CommandInclude($$); sub CommandInform($$); sub CommandIOWrite($$); sub CommandList($$); sub CommandModify($$); sub CommandReload($$); sub CommandRereadCfg($$); sub CommandRename($$); sub CommandQuit($$); sub CommandSave($$); sub CommandSet($$); sub CommandSetstate($$); sub CommandSleep($$); sub CommandShutdown($$); sub CommandTrigger($$); ################################################## # Variables: # global, to be able to access them from modules #Special values in %modules (used if set): # DefFn - define a "device" of this type # UndefFn - clean up at delete # ParseFn - Interpret a raw message # ListFn - details for this "device" # SetFn - set/activate this device # GetFn - get some data from this device # StateFn - set local info for this device, do not activate anything # NotifyFn - call this if some device changed its properties # ReadyFn - check for available data, if no FD # ReadFn - Reading from a Device (see FHZ/WS300) #Special values in %defs: # TYPE - The name of the module it belongs to # STATE - Oneliner describing its state # NR - its "serial" number # DEF - its definition # READINGS- The readings. Each value has a "VAL" and a "TIME" component. # FD - FileDescriptor. Used by selectlist / readyfnlist # IODev - attached to io device # CHANGED - Currently changed attributes of this device. Used by NotifyFn # VOLATILE- Set if the definition should be saved to the "statefile" use vars qw(%modules); # List of loaded modules (device/log/etc) use vars qw(%defs); # FHEM device/button definitions use vars qw(%attr); # Attributes use vars qw(%interfaces); # Global interface definitions, see createInterfaceDefinitions below use vars qw(%selectlist); # devices which want a "select" use vars qw(%readyfnlist); # devices which want a "readyfn" use vars qw($readytimeout); # Polling interval. UNIX: device search only $readytimeout = ($^O eq "MSWin32") ? 0.1 : 5.0; use vars qw(%value); # Current values, see commandref.html use vars qw(%oldvalue); # Old values, see commandref.html use vars qw($init_done); # use vars qw($internal_data); # use vars qw(%cmds); # Global command name hash. To be expanded use vars qw(%data); # Hash for user data use vars qw($devcount); # To sort the devices use vars qw(%defaultattr); # Default attributes, used by FHEM2FHEM use vars qw(%addNotifyCB); # Used by event enhancers (e.g. avarage) use vars qw($reread_active); my $AttrList = "room group comment alias eventMap"; my %comments; # Comments from the include files my $ipv6; # Using IPV6 my $currlogfile; # logfile, without wildcards my $currcfgfile=""; # current config/include file my $logopened = 0; # logfile opened or using stdout my %inform; # Inform hash my $rcvdquit; # Used for quit handling in init files my $sig_term = 0; # if set to 1, terminate (saving the state) my %intAt; # Internal at timer hash. my $nextat; # Time when next timer will be triggered. my $intAtCnt=0; my %duplicate; # Pool of received msg for multi-fhz/cul setups my $duplidx=0; # helper for the above pool my $cvsid = '$Id$'; my $namedef = "where is either:\n" . "- a single device name\n" . "- a list seperated by komma (,)\n" . "- a regexp, if contains one of the following characters: *[]^\$\n" . "- a range seperated by dash (-)\n"; my $stt_sec; # Used by SecondsTillTomorrow() my $stt_day; # Used by SecondsTillTomorrow() my @cmdList; # Remaining commands in a chain. Used by sleep $init_done = 0; $modules{Global}{ORDER} = -1; $modules{Global}{LOADED} = 1; $modules{Global}{AttrList} = "archivecmd apiversion archivedir configfile lastinclude logfile " . "modpath nrarchive pidfilename port statefile title userattr " . "verbose:1,2,3,4,5 mseclog version nofork logdir holiday2we " . "autoload_undefined_devices dupTimeout latitude longitude " . "backupcmd backupdir backupsymlink backup_before_update " . "exclude_from_update motd updatebranch "; $modules{Global}{AttrFn} = "GlobalAttr"; %cmds = ( "?" => { Fn=>"CommandHelp", Hlp=>",get this help" }, "attr" => { Fn=>"CommandAttr", Hlp=>" [],set attribute for "}, "define" => { Fn=>"CommandDefine", Hlp=>" ,define a device/at/notify entity" }, "deleteattr" => { Fn=>"CommandDeleteAttr", Hlp=>" [],delete attribute for " }, "delete" => { Fn=>"CommandDelete", Hlp=>",delete the corresponding definition(s)"}, "get" => { Fn=>"CommandGet", Hlp=>" ,request data from " }, "help" => { Fn=>"CommandHelp", Hlp=>",get this help" }, "include" => { Fn=>"CommandInclude", Hlp=>",read the commands from " }, "inform" => { Fn=>"CommandInform", Hlp=>"{on|timer|raw|off},echo all events to this client" }, "iowrite" => { Fn=>"CommandIOWrite", Hlp=>" ,write raw data with iodev" }, "list" => { Fn=>"CommandList", Hlp=>"[devspec],list definitions and status info" }, "modify" => { Fn=>"CommandModify", Hlp=>"device ,modify the definition (e.g. at, notify)" }, "quit" => { Fn=>"CommandQuit", Hlp=>",end the client session" }, "exit" => { Fn=>"CommandQuit", Hlp=>",end the client session" }, "reload" => { Fn=>"CommandReload", Hlp=>",reload the given module (e.g. 99_PRIV)" }, "rename" => { Fn=>"CommandRename", Hlp=>" ,rename a definition" }, "rereadcfg" => { Fn=>"CommandRereadCfg", Hlp=>",reread the config file" }, "save" => { Fn=>"CommandSave", Hlp=>"[configfile],write the configfile and the statefile" }, "set" => { Fn=>"CommandSet", Hlp=>" ,transmit code for " }, "setstate"=> { Fn=>"CommandSetstate", Hlp=>" ,set the state shown in the command list" }, "setdefaultattr" => { Fn=>"CommandDefaultAttr", Hlp=>" ,set attr for following definitions" }, "shutdown"=> { Fn=>"CommandShutdown", Hlp=>"[restart],terminate the server" }, "sleep" => { Fn=>"CommandSleep", Hlp=>",sleep for sec, 3 decimal places" }, "trigger" => { Fn=>"CommandTrigger", Hlp=>" ,trigger notify command" }, ); ################################################### # Start the program if(int(@ARGV) != 1 && int(@ARGV) != 2) { print "Usage:\n"; print "as server: fhem configfile\n"; print "as client: fhem [host:]port cmd\n"; CommandHelp(undef, undef); exit(1); } # If started as root, and there is a fhem user in the /etc/passwd, su to it if($^O !~ m/Win/ && $< == 0) { my @pw = getpwnam("fhem"); if(@pw) { use POSIX qw(setuid setgid); # set primary group setgid($pw[3]); # read all secondary groups into an array: my @groups; while ( my ($name, $pw, $gid, $members) = getgrent() ) { push(@groups, $gid) if ( grep($_ eq $pw[0],split(/\s+/,$members)) ); } # set the secondary groups via $) if (@groups) { $) = "$pw[3] ".join(" ",@groups); } else { $) = "$pw[3] $pw[3]"; } setuid($pw[2]); } } ################################################### # Client code if(int(@ARGV) == 2) { my $buf; my $addr = $ARGV[0]; $addr = "localhost:$addr" if($ARGV[0] !~ m/:/); my $client = IO::Socket::INET->new(PeerAddr => $addr); die "Can't connect to $addr\n" if(!$client); syswrite($client, "$ARGV[1] ; quit\n"); shutdown($client, 1); while(sysread($client, $buf, 256) > 0) { print($buf); } exit(0); } # End of client code ################################################### ################################################### # Server initialization doGlobalDef($ARGV[0]); # As newer Linux versions reset serial parameters after fork, we parse the # config file after the fork. Since need some global attr parameters before, we # read them here. setGlobalAttrBeforeFork($attr{global}{configfile}); if($^O =~ m/Win/ && !$attr{global}{nofork}) { Log 1, "Forcing 'attr global nofork' on WINDOWS"; Log 1, "set it in the config file to avoid this message"; $attr{global}{nofork}=1; } # Go to background if the logfile is a real file (not stdout) if($attr{global}{logfile} ne "-" && !$attr{global}{nofork}) { defined(my $pid = fork) || die "Can't fork: $!"; exit(0) if $pid; } # FritzBox special: Wait until the time is set via NTP, # but not more than 2 hours while(time() < 2*3600) { sleep(5); } my $ret = CommandInclude(undef, $attr{global}{configfile}); Log 1, "configfile: $ret" if($ret); if($attr{global}{statefile} && -r $attr{global}{statefile}) { $ret = CommandInclude(undef, $attr{global}{statefile}); Log 1, "statefile: $ret" if($ret); } SignalHandling(); my $pfn = $attr{global}{pidfilename}; if($pfn) { die "$pfn: $!\n" if(!open(PID, ">$pfn")); print PID $$ . "\n"; close(PID); } # create the global interface definitions createInterfaceDefinitions(); my $gp = $attr{global}{port}; if($gp) { Log 3, "Converting 'attr global port $gp' to 'define telnetPort telnet $gp'"; CommandDefine(undef, "telnetPort telnet $gp"); delete($attr{global}{port}); } my $sc_text = "SecurityCheck:"; $attr{global}{motd} = "$sc_text\n\n" if(!$attr{global}{motd} || $attr{global}{motd} =~ m/^$sc_text/); $init_done = 1; DoTrigger("global", "INITIALIZED"); $attr{global}{motd} .= "Running with root privileges." if($^O !~ m/Win/ && $<==0 && $attr{global}{motd} =~ m/^$sc_text/); $attr{global}{motd} .= "\nRestart fhem for a new check if the problem is fixed,\n". "or set the global attribute motd to none to supress this message.\n" if($attr{global}{motd} =~ m/^$sc_text\n\n./); my $motd = $attr{global}{motd}; if($motd eq "$sc_text\n\n") { delete($attr{global}{motd}); } else { if($motd ne "none") { $motd =~ s/\n/ /g; Log 2, $motd; } } Log 0, "Server started (version $attr{global}{version}, pid $$)"; ################################################ # Main Loop sub MAIN {MAIN:}; #Dummy my $errcount= 0; while (1) { my ($rout, $rin) = ('', ''); my $timeout = HandleTimeout(); foreach my $p (keys %selectlist) { vec($rin, $selectlist{$p}{FD}, 1) = 1; } $timeout = $readytimeout if(keys(%readyfnlist) && (!defined($timeout) || $timeout > $readytimeout)); my $nfound = select($rout=$rin, undef, undef, $timeout); CommandShutdown(undef, undef) if($sig_term); if($nfound < 0) { my $err = int($!); next if ($err == 0); Log 1, "ERROR: Select error $nfound ($err), error count= $errcount"; $errcount++; # Handling "Bad file descriptor". This is a programming error. if($err == 9) { # BADF, don't want to "use errno.ph" my $nbad = 0; foreach my $p (keys %selectlist) { my ($tin, $tout) = ('', ''); vec($tin, $selectlist{$p}{FD}, 1) = 1; if(select($tout=$tin, undef, undef, 0) < 0) { Log 1, "Found and deleted bad fileno for $p"; delete($selectlist{$p}); $nbad++; } } next if($nbad > 0); next if($errcount <= 3); } die("Select error $nfound ($err)\n"); } else { $errcount= 0; } ############################### # Message from the hardware (FHZ1000/WS3000/etc) via select or the Ready # Function. The latter ist needed for Windows, where USB devices are not # reported by select, but is used by unix too, to check if the device is # attached again. foreach my $p (keys %selectlist) { next if(!$selectlist{$p} || !$selectlist{$p}{NAME}); # due to rereadcfg/del CallFn($selectlist{$p}{NAME}, "ReadFn", $selectlist{$p}) if(vec($rout, $selectlist{$p}{FD}, 1)); } foreach my $p (keys %readyfnlist) { next if(!$readyfnlist{$p}); # due to rereadcfg / delete if(CallFn($readyfnlist{$p}{NAME}, "ReadyFn", $readyfnlist{$p})) { if($readyfnlist{$p}) { # delete itself inside ReadyFn CallFn($readyfnlist{$p}{NAME}, "ReadFn", $readyfnlist{$p}); } } } } ################################################ #Functions ahead, no more "plain" code ################################################ sub IsDummy($) { my $devname = shift; return 1 if(defined($attr{$devname}) && defined($attr{$devname}{dummy})); return 0; } sub IsIgnored($) { my $devname = shift; if($devname && defined($attr{$devname}) && defined($attr{$devname}{ignore})) { Log 4, "Ignoring $devname"; return 1; } return 0; } ################################################ sub IsIoDummy($) { my $name = shift; return IsDummy($defs{$name}{IODev}{NAME}) if($defs{$name} && $defs{$name}{IODev}); return 1; } ################################################ sub GetLogLevel(@) { my ($dev,$deflev) = @_; my $df = defined($deflev) ? $deflev : 2; return $df if(!defined($dev)); return $attr{$dev}{loglevel} if(defined($attr{$dev}) && defined($attr{$dev}{loglevel})); return $df; } ################################################ sub Log($$) { my ($loglevel, $text) = @_; return if($loglevel > $attr{global}{verbose}); my @t = localtime; my $nfile = ResolveDateWildcards($attr{global}{logfile}, @t); OpenLogfile($nfile) if(!$currlogfile || $currlogfile ne $nfile); my $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d", $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0]); if($attr{global}{mseclog}) { my ($seconds, $microseconds) = gettimeofday(); $tim .= sprintf(".%03d", $microseconds/1000); } if($logopened) { print LOG "$tim $loglevel: $text\n"; } else { print "$tim $loglevel: $text\n"; } return undef; } ##################################### sub IOWrite($@) { my ($hash, @a) = @_; my $dev = $hash->{NAME}; return if(IsDummy($dev) || IsIgnored($dev)); my $iohash = $hash->{IODev}; if(!$iohash || !$iohash->{TYPE} || !$modules{$iohash->{TYPE}} || !$modules{$iohash->{TYPE}}{WriteFn}) { Log 5, "No IO device or WriteFn found for $dev"; return; } no strict "refs"; my $ret = &{$modules{$iohash->{TYPE}}{WriteFn}}($iohash, @a); use strict "refs"; return $ret; } ##################################### sub CommandIOWrite($$) { my ($cl, $param) = @_; my @a = split(" ", $param); return "Usage: iowrite ..." if(int(@a) < 2); my $name = shift(@a); my $hash = $defs{$name}; return "$name not found" if(!$hash); return undef if(IsDummy($name) || IsIgnored($name)); if(!$hash->{TYPE} || !$modules{$hash->{TYPE}} || !$modules{$hash->{TYPE}}{WriteFn}) { Log 1, "No IO device or WriteFn found for $name"; return; } unshift(@a, "") if(int(@a) == 1); no strict "refs"; my $ret = &{$modules{$hash->{TYPE}}{WriteFn}}($hash, @a); use strict "refs"; return $ret; } ##################################### # i.e. split a line by ; (escape ;;), and execute each sub AnalyzeCommandChain($$) { my ($c, $cmd) = @_; my @ret; if($cmd =~ m/^[ \t]*(#.*)?$/) { # Save comments if(!$init_done) { if($currcfgfile ne AttrVal("global", "statefile", "")) { my $nr = $devcount++; $comments{$nr}{TEXT} = $cmd; $comments{$nr}{CFGFN} = $currcfgfile if($currcfgfile ne AttrVal("global", "configfile", "")); } } return undef; } $cmd =~ s/#.*$//s; $cmd =~ s/;;/SeMiCoLoN/g; my @saveCmdList = @cmdList; # Needed for recursive calls @cmdList = split(";", $cmd); my $subcmd; while(defined($subcmd = shift @cmdList)) { $subcmd =~ s/SeMiCoLoN/;/g; my $lret = AnalyzeCommand($c, $subcmd); push(@ret, $lret) if(defined($lret)); } @cmdList = @saveCmdList; return join("\n", @ret) if(@ret); return undef; } ##################################### sub AnalyzePerlCommand($$) { my ($cl, $cmd) = @_; $cmd =~ s/\\ *\n/ /g; # Multi-line # Make life easier for oneliners: %value = (); foreach my $d (keys %defs) { $value{$d} = $defs{$d}{STATE} } my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) = localtime; my $we = (($wday==0 || $wday==6) ? 1 : 0); if(!$we) { my $h2we = $attr{global}{holiday2we}; $we = 1 if($h2we && $value{$h2we} && $value{$h2we} ne "none"); } $month++; $year+=1900; my $ret = eval $cmd; $ret = $@ if($@); return $ret; } sub AnalyzeCommand($$) { my ($cl, $cmd) = @_; $cmd =~ s/^(\\\n|[ \t])*//;# Strip space or \\n at the begginning $cmd =~ s/[ \t]*$//; Log 5, "Cmd: >$cmd<"; return undef if(!$cmd); if($cmd =~ m/^{.*}$/s) { # Perl code return AnalyzePerlCommand($cl, $cmd); } if($cmd =~ m/^"(.*)"$/s) { # Shell code in bg, to be able to call us from it my $out = ""; $out = ">> $currlogfile 2>&1" if($currlogfile ne "-"); system("$1 $out &"); return undef; } $cmd =~ s/^[ \t]*//; my ($fn, $param) = split("[ \t][ \t]*", $cmd, 2); return undef if(!$fn); $fn = "setdefaultattr" if($fn eq "defattr"); # Compatibility mode ############# # Search for abbreviation if(!defined($cmds{$fn})) { foreach my $f (sort keys %cmds) { if(length($f) > length($fn) && substr($f, 0, length($fn)) eq $fn) { Log 5, "$fn => $f"; $fn = $f; last; } } } return "Unknown command $fn, try help" if(!defined($cmds{$fn})); $param = "" if(!defined($param)); no strict "refs"; my $ret = &{$cmds{$fn}{Fn} }($cl, $param); use strict "refs"; return undef if(defined($ret) && $ret eq ""); return $ret; } sub devspec2array($) { my %knownattr = ( "DEF"=>1, "STATE"=>1, "TYPE"=>1 ); my ($name) = @_; return "" if(!defined($name)); return $name if(defined($defs{$name})); my ($isattr, @ret); foreach my $l (split(",", $name)) { # List if($l =~ m/(.*)=(.*)/) { my ($lattr,$re) = ($1, $2); if($knownattr{$lattr}) { eval { # a bad regexp may shut down fhem.pl foreach my $l (sort keys %defs) { push @ret, $l if($defs{$l}{$lattr} && (!$re || $defs{$l}{$lattr}=~m/^$re$/)); } }; if($@) { Log 1, "devspec2array $name: $@"; return $name; } } else { foreach my $l (sort keys %attr) { push @ret, $l if($attr{$l}{$lattr} && (!$re || $attr{$l}{$lattr} =~ m/$re/)); } } $isattr = 1; next; } my $regok; eval { # a bad regexp may shut down fhem.pl if($l =~ m/[*\[\]^\$]/) { # Regexp push @ret, grep($_ =~ m/^$l$/, sort keys %defs); $regok = 1; } }; if($@) { Log 1, "devspec2array $name: $@"; return $name; } next if($regok); if($l =~ m/-/) { # Range my ($lower, $upper) = split("-", $l, 2); push @ret, grep($_ ge $lower && $_ le $upper, sort keys %defs); next; } push @ret, $l; } return $name if(!@ret && !$isattr); # No match, return the input @ret = grep { !$attr{$_} || !$attr{$_}{ignore} } @ret if($name !~ m/^ignore=/); return @ret; } ##################################### sub CommandHelp($$) { my ($cl, $param) = @_; my $str = "\n" . "Possible commands:\n\n" . "Command Parameter Description\n" . "-----------------------------------------------\n"; for my $cmd (sort keys %cmds) { my @a = split(",", $cmds{$cmd}{Hlp}, 2); $str .= sprintf("%-9s %-25s %s\n", $cmd, $a[0], $a[1]); } return $str; } ##################################### sub CommandInclude($$) { my ($cl, $arg) = @_; my $fh; my @ret; my $oldcfgfile; if(!open($fh, $arg)) { return "Can't open $arg: $!"; } Log 1, "Including $arg"; if(!$init_done && $arg ne AttrVal("global", "statefile", "") && $arg ne AttrVal("global", "configfile", "")) { my $nr = $devcount++; $comments{$nr}{TEXT} = "include $arg"; $comments{$nr}{CFGFN} = $currcfgfile if($currcfgfile ne AttrVal("global", "configfile", "")); } $oldcfgfile = $currcfgfile; $currcfgfile = $arg; my $bigcmd = ""; $rcvdquit = 0; while(my $l = <$fh>) { $l =~ s/[\r\n]//g; if($l =~ m/^(.*)\\ *$/) { # Multiline commands $bigcmd .= "$1\\\n"; } else { my $tret = AnalyzeCommandChain($cl, $bigcmd . $l); push @ret, $tret if(defined($tret)); $bigcmd = ""; } last if($rcvdquit); } $currcfgfile = $oldcfgfile; close($fh); return join("\n", @ret) if(@ret); return undef; } ##################################### sub OpenLogfile($) { my $param = shift; close(LOG); $logopened=0; $currlogfile = $param; if($currlogfile eq "-") { open LOG, '>&STDOUT' or die "Can't dup stdout: $!"; } else { $defs{global}{logfile} = $attr{global}{logfile}; $defs{global}{currentlogfile} = $param; HandleArchiving($defs{global}); open(LOG, ">>$currlogfile") || return("Can't open $currlogfile: $!"); redirectStdinStdErr() if($init_done); } LOG->autoflush(1); $logopened = 1; return undef; } sub redirectStdinStdErr() { # Redirect stdin/stderr return if(!$currlogfile || $currlogfile eq "-"); open STDIN, '>$currlogfile") or print "Can't append STDERR to log: $!\n"; STDERR->autoflush(1); close(STDOUT); open STDOUT, '>&STDERR' or print "Can't dup stdout: $!\n"; STDOUT->autoflush(1); } ##################################### sub CommandRereadCfg($$) { my ($cl, $param) = @_; my $name = $cl->{NAME} if($cl); WriteStatefile(); $reread_active=1; $init_done = 0; foreach my $d (keys %defs) { my $ret = CallFn($d, "UndefFn", $defs{$d}, $d) if($name && $name ne $d); return $ret if($ret); } my $cfgfile = $attr{global}{configfile}; %comments = (); %defs = (); %attr = (); %selectlist = (); %readyfnlist = (); %inform = (); doGlobalDef($cfgfile); setGlobalAttrBeforeFork($cfgfile); my $ret = CommandInclude($cl, $cfgfile); if($attr{global}{statefile} && -r $attr{global}{statefile}) { my $ret2 = CommandInclude($cl, $attr{global}{statefile}); $ret = (defined($ret) ? "$ret\n$ret2" : $ret2) if(defined($ret2)); } DoTrigger("global", "REREADCFG"); $defs{$name} = $selectlist{$name} = $cl if($name); $init_done = 1; $reread_active=0; return $ret; } ##################################### sub CommandQuit($$) { my ($cl, $param) = @_; if(!$cl) { $rcvdquit = 1; } else { $cl->{rcvdQuit} = 1; return "Bye..." if($cl->{prompt}); } return undef; } ##################################### sub WriteStatefile() { return "No statefile specified" if(!$attr{global}{statefile}); if(!open(SFH, ">$attr{global}{statefile}")) { my $msg = "WriteStateFile: Cannot open $attr{global}{statefile}: $!"; Log 1, $msg; return $msg; } my $t = localtime; print SFH "#$t\n"; foreach my $d (sort keys %defs) { next if($defs{$d}{TEMPORARY}); print SFH "define $d $defs{$d}{TYPE} $defs{$d}{DEF}\n" if($defs{$d}{VOLATILE}); my $val = $defs{$d}{STATE}; if(defined($val) && $val ne "unknown" && $val ne "Initialized" && $val ne "???") { $val =~ s/;/;;/g; print SFH "setstate $d $val\n" } ############# # Now the detailed list my $r = $defs{$d}{READINGS}; if($r) { foreach my $c (sort keys %{$r}) { my $rd = $r->{$c}; if(!defined($rd->{TIME})) { Log 4, "WriteStatefile $d $c: Missing TIME, using current time"; $rd->{TIME} = TimeNow(); } if(!defined($rd->{VAL})) { Log 4, "WriteStatefile $d $c: Missing VAL, setting it to 0"; $rd->{VAL} = 0; } my $val = $rd->{VAL}; $val =~ s/;/;;/g; print SFH "setstate $d $rd->{TIME} $c $val\n"; } } } close(SFH); return ""; } ##################################### sub CommandSave($$) { my ($cl, $param) = @_; my $ret = ""; WriteStatefile(); $param = $attr{global}{configfile} if(!$param); return "No configfile attribute set and no argument specified" if(!$param); if(!open(SFH, ">$param")) { return "Cannot open $param: $!"; } my %fh = ("configfile" => *SFH); my %skip; my %devByNr; map { $devByNr{$defs{$_}{NR}} = $_ } keys %defs; for(my $i = 0; $i < $devcount; $i++) { my ($h, $d); if($comments{$i}) { $h = $comments{$i}; } else { $d = $devByNr{$i}; next if(!defined($d) || $defs{$d}{TEMPORARY} || # e.g. WEBPGM connections $defs{$d}{VOLATILE}); # e.g at, will be saved to the statefile $h = $defs{$d}; } my $cfgfile = $h->{CFGFN} ? $h->{CFGFN} : "configfile"; my $fh = $fh{$cfgfile}; if(!$fh) { if(!open($fh, ">$cfgfile")) { $ret .= "Cannot open $cfgfile: $!, ignoring its content\n"; $fh{$cfgfile} = 1; $skip{$cfgfile} = 1; } else { $fh{$cfgfile} = $fh; } } next if($skip{$cfgfile}); if(!defined($d)) { print $fh $h->{TEXT},"\n"; next; } if($d ne "global") { my $def = $defs{$d}{DEF}; if(defined($def)) { $def =~ s/;/;;/g; print $fh "define $d $defs{$d}{TYPE} $def\n"; } else { print $fh "define $d $defs{$d}{TYPE}\n"; } } foreach my $a (sort keys %{$attr{$d}}) { next if($d eq "global" && ($a eq "configfile" || $a eq "version")); my $val = $attr{$d}{$a}; $val =~ s/;/;;/g; $val =~ s/\n/\\\n/g; print $fh "attr $d $a $val\n"; } } print SFH "include $attr{global}{lastinclude}\n" if($attr{global}{lastinclude}); foreach my $fh (values %fh) { close($fh) if($fh ne "1"); } return ($ret ? $ret : undef); } ##################################### sub CommandShutdown($$) { my ($cl, $param) = @_; DoTrigger("global", "SHUTDOWN"); Log 0, "Server shutdown"; foreach my $d (sort keys %defs) { CallFn($d, "ShutdownFn", $defs{$d}); } WriteStatefile(); unlink($attr{global}{pidfilename}) if($attr{global}{pidfilename}); if($param && $param eq "restart") { system("(sleep 2; exec $^X $0 $attr{global}{configfile})&"); } exit(0); } ##################################### sub DoSet(@) { my @a = @_; my $dev = $a[0]; return "Please define $dev first" if(!$defs{$dev}); return "No set implemented for $dev" if(!$modules{$defs{$dev}{TYPE}}{SetFn}); @a = ReplaceEventMap($dev, \@a, 0) if($attr{$dev}{eventMap}); my $ret = CallFn($dev, "SetFn", $defs{$dev}, @a); return $ret if($ret); shift @a; my $arg = $defs{$dev}{CHANGED} ? undef : join(" ", @a); return DoTrigger($dev, $arg); } ##################################### sub CommandSet($$) { my ($cl, $param) = @_; my @a = split("[ \t][ \t]*", $param); return "Usage: set \n$namedef" if(int(@a)<1); my @rets; foreach my $sdev (devspec2array($a[0])) { $a[0] = $sdev; my $ret = DoSet(@a); push @rets, $ret if($ret); } return join("\n", @rets); } ##################################### sub CommandGet($$) { my ($cl, $param) = @_; my @a = split("[ \t][ \t]*", $param); return "Usage: get \n$namedef" if(int(@a) < 1); my @rets; foreach my $sdev (devspec2array($a[0])) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } if(!$modules{$defs{$sdev}{TYPE}}{GetFn}) { push @rets, "No get implemented for $sdev"; next; } $a[0] = $sdev; my $ret = CallFn($sdev, "GetFn", $defs{$sdev}, @a); push @rets, $ret if($ret); } return join("\n", @rets); } ##################################### sub LoadModule($) { my ($m) = @_; if($modules{$m} && !$modules{$m}{LOADED}) { # autoload my $o = $modules{$m}{ORDER}; my $ret = CommandReload(undef, "${o}_$m"); if($ret) { Log 0, $ret; return "UNDEFINED"; } if(!$modules{$m}{LOADED}) { # Case corrected by reload? foreach my $i (keys %modules) { if(uc($m) eq uc($i) && $modules{$i}{LOADED}) { delete($modules{$m}); $m = $i; last; } } } } return $m; } ##################################### sub CommandDefine($$) { my ($cl, $def) = @_; my @a = split("[ \t][ \t]*", $def, 3); my $name = $a[0]; return "Usage: define " if(int(@a) < 2); return "$name already defined, delete it first" if(defined($defs{$name})); return "Invalid characters in name (not A-Za-z0-9.:_): $name" if($name !~ m/^[a-z0-9.:_]*$/i); my $m = $a[1]; if(!$modules{$m}) { # Perhaps just wrong case? foreach my $i (keys %modules) { if(uc($m) eq uc($i)) { $m = $i; last; } } } my $newm = LoadModule($m); return "Cannot load module $m" if($newm eq "UNDEFINED"); $m = $newm; if(!$modules{$m} || !$modules{$m}{DefFn}) { my @m = grep { $modules{$_}{DefFn} || !$modules{$_}{LOADED} } sort keys %modules; return "Unknown module $m, choose one of @m"; } my %hash; $hash{NAME} = $name; $hash{TYPE} = $m; $hash{STATE} = "???"; $hash{DEF} = $a[2] if(int(@a) > 2); $hash{NR} = $devcount++; $hash{CFGFN} = $currcfgfile if($currcfgfile ne AttrVal("global", "configfile", "")); # If the device wants to issue initialization gets/sets, then it needs to be # in the global hash. $defs{$name} = \%hash; my $ret = CallFn($name, "DefFn", \%hash, $def); if($ret) { Log 1, "define: $ret"; delete $defs{$name}; # Veto delete $attr{$name}; } else { foreach my $da (sort keys (%defaultattr)) { # Default attributes CommandAttr($cl, "$name $da $defaultattr{$da}"); } DoTrigger("global", "DEFINED $name"); if($modules{$m}{NotifyFn} && !$hash{NTFY_ORDER}) { $hash{NTFY_ORDER} = ($modules{$m}{NotifyOrderPrefix} ? $modules{$m}{NotifyOrderPrefix} : "50-") . $name; } } return $ret; } ##################################### sub CommandModify($$) { my ($cl, $def) = @_; my @a = split("[ \t]+", $def, 2); return "Usage: modify " if(int(@a) < 2); # Return a list of modules return "Define $a[0] first" if(!defined($defs{$a[0]})); my $hash = $defs{$a[0]}; $hash->{OLDDEF} = $hash->{DEF}; $hash->{DEF} = $a[1]; my $ret = CallFn($a[0], "DefFn", $hash, "$a[0] $hash->{TYPE} $a[1]"); $hash->{DEF} = $hash->{OLDDEF} if($ret); delete($hash->{OLDDEF}); return $ret; } ############# # internal sub AssignIoPort($) { my ($hash) = @_; # Set the I/O device, search for the last compatible one. for my $p (sort { $defs{$b}{NR} <=> $defs{$a}{NR} } keys %defs) { my $cl = $defs{$p}{Clients}; $cl = $modules{$defs{$p}{TYPE}}{Clients} if(!$cl); if((defined($cl) && $cl =~ m/:$hash->{TYPE}:/) && $defs{$p}{NAME} ne $hash->{NAME}) { # e.g. RFR $hash->{IODev} = $defs{$p}; last; } } Log 3, "No I/O device found for $hash->{NAME}" if(!$hash->{IODev}); } ############# sub CommandDelete($$) { my ($cl, $def) = @_; return "Usage: delete $namedef\n" if(!$def); my @rets; foreach my $sdev (devspec2array($def)) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } my $ret = CallFn($sdev, "UndefFn", $defs{$sdev}, $sdev); if($ret) { push @rets, $ret; next; } # Delete releated hashes foreach my $p (keys %selectlist) { if($selectlist{$p} && $selectlist{$p}{NAME} eq $sdev) { delete $selectlist{$p}; } } foreach my $p (keys %readyfnlist) { delete $readyfnlist{$p} if($readyfnlist{$p} && $readyfnlist{$p}{NAME} eq $sdev); } delete($attr{$sdev}); my $temporary = $defs{$sdev}{TEMPORARY}; delete($defs{$sdev}); # Remove the main entry DoTrigger("global", "DELETED $sdev") if(!$temporary); } return join("\n", @rets); } ############# sub CommandDeleteAttr($$) { my ($cl, $def) = @_; my @a = split(" ", $def, 2); return "Usage: deleteattr []\n$namedef" if(@a < 1); my @rets; foreach my $sdev (devspec2array($a[0])) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } $a[0] = $sdev; $ret = CallFn($sdev, "AttrFn", "del", @a); if($ret) { push @rets, $ret; next; } if(@a == 1) { delete($attr{$sdev}); } else { delete($attr{$sdev}{$a[1]}) if(defined($attr{$sdev})); } } return join("\n", @rets); } sub PrintHash($$) { my ($h, $lev) = @_; my ($str,$sstr) = ("",""); foreach my $c (sort keys %{$h}) { if(ref($h->{$c})) { if(ref($h->{$c}) eq "HASH") { if(defined($h->{$c}{TIME}) && defined($h->{$c}{VAL})) { $str .= sprintf("%*s %-19s %-15s %s\n", $lev," ", $h->{$c}{TIME},$c,$h->{$c}{VAL}); } elsif($c eq "IODev" || $c eq "HASH") { $str .= sprintf("%*s %-10s %s\n", $lev," ",$c, $h->{$c}{NAME}); } else { $sstr .= sprintf("%*s %s:\n", $lev, " ", uc(substr($c,0,1)).lc(substr($c,1))); $sstr .= PrintHash($h->{$c}, $lev+2); } } elsif(ref($h->{$c}) eq "ARRAY") { $sstr .= sprintf("%*s %s:\n", $lev, " ", $c); foreach my $v (@{$h->{$c}}) { $sstr .= sprintf("%*s %s\n", $lev+2, " ", $v); } } } else { $str .= sprintf("%*s %-10s %s\n", $lev," ",$c, $h->{$c}); } } return $str . $sstr; } ##################################### sub CommandList($$) { my ($cl, $param) = @_; my $str = ""; if(!$param) { $str = "\nType list for detailed info.\n"; my $lt = ""; # Sort first by type then by name for my $d (sort { my $x=$modules{$defs{$a}{TYPE}}{ORDER}.$defs{$a}{TYPE} cmp $modules{$defs{$b}{TYPE}}{ORDER}.$defs{$b}{TYPE}; $x=($a cmp $b) if($x == 0); $x; } keys %defs) { next if(IsIgnored($d)); my $t = $defs{$d}{TYPE}; $str .= "\n$t:\n" if($t ne $lt); $str .= sprintf(" %-20s (%s)\n", $d, $defs{$d}{STATE}); $lt = $t; } } else { my @arg = split(" ", $param); my @list = devspec2array($arg[0]); if($arg[1]) { foreach my $sdev (@list) { if($defs{$sdev} && $defs{$sdev}{$arg[1]}) { $str .= $sdev . " " . $defs{$sdev}{$arg[1]} . "\n"; } elsif($defs{$sdev} && $defs{$sdev}{READINGS} && $defs{$sdev}{READINGS}{$arg[1]}) { $str .= $sdev . " ". $defs{$sdev}{READINGS}{$arg[1]}{TIME} . " " . $defs{$sdev}{READINGS}{$arg[1]}{VAL} . "\n"; } } } elsif(@list == 1) { my $sdev = $list[0]; if(!defined($defs{$sdev})) { $str .= "No device named $param found"; } else { $str .= "Internals:\n"; $str .= PrintHash($defs{$sdev}, 2); $str .= "Attributes:\n"; $str .= PrintHash($attr{$sdev}, 2); } } else { foreach my $sdev (@list) { $str .= "$sdev\n"; } } } return $str; } ##################################### sub CommandReload($$) { my ($cl, $param) = @_; my %hash; $param =~ s,/,,g; $param =~ s,\.pm$,,g; my $file = "$attr{global}{modpath}/FHEM/$param.pm"; return "Can't read $file: $!" if(! -r "$file"); my $m = $param; $m =~ s,^([0-9][0-9])_,,; my $order = (defined($1) ? $1 : "00"); Log 5, "Loading $file"; no strict "refs"; my $ret = eval { my $ret=do "$file"; if(!$ret) { Log 1, "reload: Error:Modul $param deactivated:\n $@"; return $@; } # Get the name of the initialize function. This may differ from the # filename as sometimes we live on a FAT fs with wrong case. my $fnname = $m; foreach my $i (keys %main::) { if($i =~ m/^(${m})_initialize$/i) { $fnname = $1; last; } } &{ "${fnname}_Initialize" }(\%hash); $m = $fnname; return undef; }; use strict "refs"; return "$@" if($@); return $ret if($ret); my ($defptr, $ldata); if($modules{$m}) { $defptr = $modules{$m}{defptr}; $ldata = $modules{$m}{ldata}; } $modules{$m} = \%hash; $modules{$m}{ORDER} = $order; $modules{$m}{LOADED} = 1; $modules{$m}{defptr} = $defptr if($defptr); $modules{$m}{ldata} = $defptr if($ldata); return undef; } ##################################### sub CommandRename($$) { my ($cl, $param) = @_; my ($old, $new) = split(" ", $param); return "Please define $old first" if(!defined($defs{$old})); return "Invalid characters in name (not A-Za-z0-9.:_): $new" if($new !~ m/^[a-z0-9.:_]*$/i); return "Cannot rename global" if($old eq "global"); $defs{$new} = $defs{$old}; $defs{$new}{NAME} = $new; delete($defs{$old}); # The new pointer will preserve the hash $attr{$new} = $attr{$old} if(defined($attr{$old})); delete($attr{$old}); $oldvalue{$new} = $oldvalue{$old} if(defined($oldvalue{$old})); delete($oldvalue{$old}); DoTrigger("global", "RENAMED $old $new"); return undef; } ##################################### sub getAllAttr($) { my $d = shift; return "" if(!$defs{$d}); my $list = $AttrList; $list .= " " . $modules{$defs{$d}{TYPE}}{AttrList} if($modules{$defs{$d}{TYPE}}{AttrList}); $list .= " " . $attr{global}{userattr} if($attr{global}{userattr}); return $list; } ##################################### sub getAllSets($) { my $d = shift; if(AttrVal("global", "apiversion", 1)> 1) { my @setters= getSetters($defs{$d}); return join(" ", @setters); } my $a2 = CommandSet(undef, "$d ?"); $a2 =~ s/.*choose one of //; $a2 = "" if($a2 =~ /^No set implemented for/); my $em = AttrVal($d, "eventMap", undef); if($em) { $em = join(" ", grep { !/ / } map { $_ =~ s/.*://s; $_ } EventMapAsList($em)); $a2 = "$em $a2"; } return $a2; } sub GlobalAttr($$) { my ($type, $me, $name, $val) = @_; return if($type ne "set"); ################ if($name eq "logfile") { my @t = localtime; my $ret = OpenLogfile(ResolveDateWildcards($val, @t)); if($ret) { return $ret if($init_done); die($ret); } } ################ elsif($name eq "verbose") { if($val =~ m/^[0-5]$/) { return undef; } else { $attr{global}{verbose} = 3; return "Valid value for verbose are 0,1,2,3,4,5"; } } elsif($name eq "modpath") { return "modpath must point to a directory where the FHEM subdir is" if(! -d "$val/FHEM"); my $modpath = "$val/FHEM"; opendir(DH, $modpath) || return "Can't read $modpath: $!"; push @INC, $modpath if(!grep(/$modpath/, @INC)); eval { use vars qw($DISTRIB_DESCRIPTION); require "FhemUtils/release.pm"; $attr{global}{version} = "$DISTRIB_DESCRIPTION, $cvsid"; }; my $counter = 0; foreach my $m (sort readdir(DH)) { next if($m !~ m/^([0-9][0-9])_(.*)\.pm$/); $modules{$2}{ORDER} = $1; CommandReload(undef, $m) # Always load utility modules if($1 eq "99" && !$modules{$2}{LOADED}); $counter++; } closedir(DH); if(!$counter) { return "No modules found, set modpath to a directory in which a " . "subdirectory called \"FHEM\" exists wich in turn contains " . "the fhem module files <*>.pm"; } } return undef; } ##################################### sub CommandAttr($$) { my ($cl, $param) = @_; my $ret = undef; my @a; @a = split(" ", $param, 3) if($param); return "Usage: attr []\n$namedef" if(@a && @a < 2); my @rets; foreach my $sdev (devspec2array($a[0])) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } my $list = getAllAttr($sdev); if($a[1] eq "?") { push @rets, "$sdev: unknown attribute $a[1], choose one of $list"; next; } if(" $list " !~ m/ ${a[1]}[ :;]/) { my $found = 0; foreach my $atr (split("[ \t]", $list)) { # is it a regexp? if(${a[1]} =~ m/^$atr$/) { $found++; last; } } if(!$found) { push @rets, "$sdev: unknown attribute $a[1], ". "choose one of $list or use attr global userattr $a[1]"; next; } } if($a[1] eq "IODev" && (!$a[2] || !defined($defs{$a[2]}))) { push @rets,"$sdev: unknown IODev specified"; next; } $a[0] = $sdev; $ret = CallFn($sdev, "AttrFn", "set", @a); if($ret) { push @rets, $ret; next; } if(defined($a[2])) { $attr{$sdev}{$a[1]} = $a[2]; } else { $attr{$sdev}{$a[1]} = "1"; } if($a[1] eq "IODev") { my $ioname = $a[2]; $defs{$sdev}{IODev} = $defs{$ioname}; $defs{$sdev}{NR} = $devcount++ if($defs{$ioname}{NR} > $defs{$sdev}{NR}); } } Log 3, join(" ", @rets) if(!$cl && @rets); return join("\n", @rets); } ##################################### # Default Attr sub CommandDefaultAttr($$) { my ($cl, $param) = @_; my @a = split(" ", $param, 2); if(int(@a) == 0) { %defaultattr = (); } elsif(int(@a) == 1) { $defaultattr{$a[0]} = 1; } else { $defaultattr{$a[0]} = $a[1]; } return undef; } ##################################### sub CommandSetstate($$) { my ($cl, $param) = @_; my @a = split(" ", $param, 2); return "Usage: setstate \n$namedef" if(@a != 2); my @rets; foreach my $sdev (devspec2array($a[0])) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } my $d = $defs{$sdev}; # Detailed state with timestamp if($a[1] =~ m/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) +([^ ].*)$/) { my ($tim, $nameval) = ($1, $2); my ($sname, $sval) = split(" ", $nameval, 2); (undef, $sval) = ReplaceEventMap($sdev, [$sdev, $sval], 0) if($attr{$sdev}{eventMap}); my $ret = CallFn($sdev, "StateFn", $d, $tim, $sname, $sval); if($ret) { push @rets, $ret; next; } if(!$d->{READINGS}{$sname} || $d->{READINGS}{$sname}{TIME} lt $tim) { $d->{READINGS}{$sname}{VAL} = $sval; $d->{READINGS}{$sname}{TIME} = $tim; } } else { # Do not overwrite state like "opened" or "initialized" $d->{STATE} = $a[1] if($init_done || $d->{STATE} eq "???"); # This time is not the correct one, but we do not store a timestamp for # this reading. my $tn = TimeNow(); $oldvalue{$sdev}{TIME} = $tn; $oldvalue{$sdev}{VAL} = $d->{STATE}; my $ret = CallFn($sdev, "StateFn", $d, $tn, "STATE", $a[1]); if($ret) { push @rets, $ret; next; } } } return join("\n", @rets); } ##################################### sub CommandTrigger($$) { my ($cl, $param) = @_; my ($dev, $state) = split(" ", $param, 2); return "Usage: trigger \n$namedef" if(!$dev); $state = "" if(!$state); my @rets; foreach my $sdev (devspec2array($dev)) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; next; } my $ret = DoTrigger($sdev, $state); if($ret) { push @rets, $ret; next; } } return join("\n", @rets); } ##################################### sub CommandInform($$) { my ($cl, $param) = @_; return if(!$cl); my $name = $cl->{NAME}; return "Usage: inform {on|timer|raw|off} [regexp]" if($param !~ m/^(on|off|raw|timer)/); delete($inform{$name}); if($param !~ m/^off/) { my ($type, $regexp) = split(" ", $param); $inform{$name}{NR} = $cl->{NR}; $inform{$name}{type} = $type; if($regexp) { eval { "Hallo" =~ m/$regexp/ }; return "Bad regexp: $@" if($@); $inform{$name}{regexp} = $regexp; } Log 4, "Setting inform to $param"; } return undef; } ##################################### sub CommandSleep($$) { my ($cl, $param) = @_; return "Cannot interpret $param as seconds" if($param !~ m/^[0-9\.]+$/); Log 4, "sleeping for $param"; if(!$cl && @cmdList && $param && $init_done) { InternalTimer(gettimeofday()+$param, "WakeUpFn", join(";", @cmdList), 0); @cmdList=(); } else { select(undef, undef, undef, $param); } return undef; } sub WakeUpFn($) { my $param = shift; my $ret = AnalyzeCommandChain(undef, $param); Log 2, "After sleep: $ret" if($ret); } ##################################### # Return the time to the next event (or undef if there is none) # and call each function which was scheduled for this time sub HandleTimeout() { return undef if(!$nextat); my $now = gettimeofday(); return ($nextat-$now) if($now < $nextat); $nextat = 0; ############# # Check the internal list. foreach my $i (sort { $intAt{$a}{TRIGGERTIME} <=> $intAt{$b}{TRIGGERTIME} } keys %intAt) { my $tim = $intAt{$i}{TRIGGERTIME}; my $fn = $intAt{$i}{FN}; if(!defined($tim) || !defined($fn)) { delete($intAt{$i}); next; } elsif($tim <= $now) { no strict "refs"; &{$fn}($intAt{$i}{ARG}); use strict "refs"; delete($intAt{$i}); } $nextat = $tim if(!$nextat || $nextat > $tim); } return undef if(!$nextat); $now = gettimeofday(); return ($now < $nextat) ? ($nextat-$now) : 0; } ##################################### sub InternalTimer($$$$) { my ($tim, $fn, $arg, $waitIfInitNotDone) = @_; if(!$init_done && $waitIfInitNotDone) { select(undef, undef, undef, $tim-gettimeofday()); no strict "refs"; &{$fn}($arg); use strict "refs"; return; } $intAt{$intAtCnt}{TRIGGERTIME} = $tim; $intAt{$intAtCnt}{FN} = $fn; $intAt{$intAtCnt}{ARG} = $arg; $intAtCnt++; $nextat = $tim if(!$nextat || $nextat > $tim); } ##################################### sub RemoveInternalTimer($) { my ($arg) = @_; foreach my $a (keys %intAt) { delete($intAt{$a}) if($intAt{$a}{ARG} eq $arg); } } ##################################### sub SignalHandling() { if($^O ne "MSWin32") { $SIG{'INT'} = sub { $sig_term = 1; }; $SIG{'TERM'} = sub { $sig_term = 1; }; $SIG{'PIPE'} = 'IGNORE'; $SIG{'CHLD'} = 'IGNORE'; $SIG{'HUP'} = sub { CommandRereadCfg(undef, "") }; } } ##################################### sub TimeNow() { my @t = localtime; return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); } ##################################### sub FmtDateTime($) { my @t = localtime(shift); return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); } sub FmtTime($) { my @t = localtime(shift); return sprintf("%02d:%02d:%02d", $t[2], $t[1], $t[0]); } ##################################### sub CommandChain($$) { my ($retry, $list) = @_; my $ov = $attr{global}{verbose}; my $oid = $init_done; $init_done = 0; # Rudi: ??? $attr{global}{verbose} = 1; foreach my $cmd (@{$list}) { for(my $n = 0; $n < $retry; $n++) { Log 1, sprintf("Trying again $cmd (%d out of %d)", $n+1,$retry) if($n>0); my $ret = AnalyzeCommand(undef, $cmd); last if(!defined($ret) || $ret !~ m/Timeout/); } } $attr{global}{verbose} = $ov; $init_done = $oid; } ##################################### sub ResolveDateWildcards($@) { use POSIX qw(strftime); my ($f, @t) = @_; return $f if(!$f); return $f if($f !~ m/%/); # Be fast if there is no wildcard $f =~ s/%L/$attr{global}{logdir}/g if($attr{global}{logdir}); #log directory return strftime($f,@t); } sub SemicolonEscape($) { my $cmd = shift; $cmd =~ s/^[ \t]*//; $cmd =~ s/[ \t]*$//; if($cmd =~ m/^{.*}$/s || $cmd =~ m/^".*"$/s) { $cmd =~ s/;/;;/g } return $cmd; } sub EvalSpecials($%) { # The character % will be replaced with the received event, # e.g. with on or off or measured-temp: 21.7 (Celsius) # The character @ will be replaced with the device name. # To use % or @ in the text itself, use the double mode (%% or @@). # Instead of % and @, the parameters %EVENT (same as %), # %NAME (same as @) and %TYPE (contains the device type, e.g. FHT) # can be used. A single % looses its special meaning if any of these # parameters appears in the definition. my ($exec, %specials)= @_; $exec = SemicolonEscape($exec); $exec =~ s/%%/____/g; # %EVTPART due to HM remote logic my $idx = 0; if(defined($specials{"%EVENT"})) { foreach my $part (split(" ", $specials{"%EVENT"})) { $specials{"%EVTPART$idx"} = $part; $idx++; } } # perform macro substitution my $extsyntax= 0; foreach my $special (keys %specials) { $extsyntax+= ($exec =~ s/$special/$specials{$special}/g); } if(!$extsyntax) { $exec =~ s/%/$specials{"%EVENT"}/g; } $exec =~ s/____/%/g; $exec =~ s/@@/____/g; $exec =~ s/@/$specials{"%NAME"}/g; $exec =~ s/____/@/g; return $exec; } ##################################### # Parse a timespec: Either HH:MM:SS or HH:MM or { perfunc() } sub GetTimeSpec($) { my ($tspec) = @_; my ($hr, $min, $sec, $fn); if($tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { ($hr, $min, $sec) = ($1, $2, $3); } elsif($tspec =~ m/^([0-9]+):([0-5][0-9])$/) { ($hr, $min, $sec) = ($1, $2, 0); } elsif($tspec =~ m/^{(.*)}$/) { $fn = $1; $tspec = AnalyzeCommand(undef, "{$fn}"); if(!$@ && $tspec =~ m/^([0-9]+):([0-5][0-9]):([0-5][0-9])$/) { ($hr, $min, $sec) = ($1, $2, $3); } elsif(!$@ && $tspec =~ m/^([0-9]+):([0-5][0-9])$/) { ($hr, $min, $sec) = ($1, $2, 0); } else { $tspec = "" if(!$tspec); return ("the at function \"$fn\" must return a timespec and not $tspec.", undef, undef, undef, undef); } } else { return ("Wrong timespec $tspec: either HH:MM:SS or {perlcode}", undef, undef, undef, undef); } return (undef, $hr, $min, $sec, $fn); } ##################################### # Do the notification sub DoTrigger($$) { my ($dev, $ns) = @_; my $ret = ""; return "" if(!defined($defs{$dev})); if(defined($ns)) { if($defs{$dev}{CHANGED}) { push @{$defs{$dev}{CHANGED}}, $ns; } else { $defs{$dev}{CHANGED}[0] = $ns; } } elsif(!defined($defs{$dev}{CHANGED})) { return ""; } if($attr{$dev}{eventMap}) { my $c = $defs{$dev}{CHANGED}; for(my $i = 0; $i < @{$c}; $i++) { $c->[$i] = ReplaceEventMap($dev, $c->[$i], 1); } $defs{$dev}{STATE} = ReplaceEventMap($dev, $defs{$dev}{STATE}, 1); } # STATE && {READINGS}{state} should be the same my $r = $defs{$dev}{READINGS}; $r->{state}{VAL} = $defs{$dev}{STATE} if($r && $r->{state}); my $max = int(@{$defs{$dev}{CHANGED}}); Log 5, "Triggering $dev ($max changes)"; return "" if(defined($attr{$dev}) && defined($attr{$dev}{do_not_notify})); ################ # Log/notify modules # If modifying a device in its own trigger, do not call the triggers from # the inner loop. if(!defined($defs{$dev}{INTRIGGER})) { $defs{$dev}{INTRIGGER}=1; my @ntfyList = sort { $defs{$a}{NTFY_ORDER} cmp $defs{$b}{NTFY_ORDER} } grep { $defs{$_}{NTFY_ORDER} } keys %defs; foreach my $n (@ntfyList) { next if(!defined($defs{$n})); # Was deleted in a previous notify Log 5, "$dev trigger: Checking $n for notify"; my $r = CallFn($n, "NotifyFn", $defs{$n}, $defs{$dev}); $ret .= $r if($r); } ################ # Inform if($defs{$dev}{CHANGED}) { # It gets deleted sometimes (?) $max = int(@{$defs{$dev}{CHANGED}}); # can be enriched in the notifies foreach my $c (keys %inform) { if(!$defs{$c} || $defs{$c}{NR} != $inform{$c}{NR}) { delete($inform{$c}); next; } next if($inform{$c}{type} eq "raw"); my $tn = TimeNow(); if($attr{global}{mseclog}) { my ($seconds, $microseconds) = gettimeofday(); $tn .= sprintf(".%03d", $microseconds/1000); } my $re = $inform{$c}{regexp}; for(my $i = 0; $i < $max; $i++) { my $state = $defs{$dev}{CHANGED}[$i]; next if($re && !($dev =~ m/$re/ || "$dev:$state" =~ m/$re/)); syswrite($defs{$c}{CD}, ($inform{$c}{type} eq "timer" ? "$tn " : "") . "$defs{$dev}{TYPE} $dev $state\n"); } } } delete($defs{$dev}{INTRIGGER}); } #################### # Used by triggered perl programs to check the old value # Not suited for multi-valued devices (KS300, etc) $oldvalue{$dev}{TIME} = TimeNow(); $oldvalue{$dev}{VAL} = $defs{$dev}{STATE}; delete($defs{$dev}{CHANGED}) if(!defined($defs{$dev}{INTRIGGER})); Log 3, "NTFY return: $ret" if($ret); return $ret; } ##################################### # Wrapper for calling a module function sub CallFn(@) { my $d = shift; my $n = shift; if(!$defs{$d}) { Log 0, "Strange call for nonexistent $d: $n"; return undef; } if(!$defs{$d}{TYPE}) { Log 0, "Strange call for typeless $d: $n"; return undef; } my $fn = $modules{$defs{$d}{TYPE}}{$n}; return "" if(!$fn); no strict "refs"; my $ret = &{$fn}(@_); use strict "refs"; return $ret; } ##################################### # Used from perl oneliners inside of scripts sub fhem($) { my $param = shift; my $ret = AnalyzeCommandChain(undef, $param); Log 3, "$param : $ret" if($ret); return $ret; } ##################################### # initialize the global device sub doGlobalDef($) { my ($arg) = @_; $devcount = 1; $defs{global}{NR} = $devcount++; $defs{global}{TYPE} = "Global"; $defs{global}{STATE} = ""; $defs{global}{DEF} = ""; $defs{global}{NAME} = "global"; CommandAttr(undef, "global verbose 3"); CommandAttr(undef, "global configfile $arg"); CommandAttr(undef, "global logfile -"); } ##################################### # rename does not work over Filesystems: lets copy it sub myrename($$) { my ($from, $to) = @_; if(!open(F, $from)) { Log(1, "Rename: Cannot open $from: $!"); return; } if(!open(T, ">$to")) { Log(1, "Rename: Cannot open $to: $!"); return; } while(my $l = ) { print T $l; } close(F); close(T); unlink($from); } ##################################### # Make a directory and its parent directories if needed. sub HandleArchiving($) { my ($log) = @_; my $ln = $log->{NAME}; return if(!$attr{$ln}); # If there is a command, call that my $cmd = $attr{$ln}{archivecmd}; if($cmd) { $cmd =~ s/%/$log->{currentlogfile}/g; Log 2, "Archive: calling $cmd"; system($cmd); return; } my $nra = $attr{$ln}{nrarchive}; my $ard = $attr{$ln}{archivedir}; return if(!defined($nra)); # If nrarchive is set, then check the last files: # Get a list of files: my ($dir, $file); if($log->{logfile} =~ m,^(.+)/([^/]+)$,) { ($dir, $file) = ($1, $2); } else { ($dir, $file) = (".", $log->{logfile}); } $file =~ s/%./.+/g; return if(!opendir(DH, $dir)); my @files = sort grep {/^$file$/} readdir(DH); closedir(DH); my $max = int(@files)-$nra; for(my $i = 0; $i < $max; $i++) { if($ard) { Log 2, "Moving $files[$i] to $ard"; myrename("$dir/$files[$i]", "$ard/$files[$i]"); } else { Log 2, "Deleting $files[$i]"; unlink("$dir/$files[$i]"); } } } ##################################### # Call a logical device (FS20) ParseMessage with data from a physical device # (FHZ) sub Dispatch($$$) { my ($hash, $dmsg, $addvals) = @_; my $iohash = $modules{$hash->{TYPE}}; # The phyiscal device module pointer my $name = $hash->{NAME}; Log 5, "$name dispatch $dmsg"; my ($isdup, $idx) = CheckDuplicate($name, $dmsg); if($isdup) { my $found = $duplicate{$idx}{FND}; foreach my $found (@{$found}) { if($addvals) { foreach my $av (keys %{$addvals}) { $defs{$found}{"${name}_$av"} = $addvals->{$av}; } } $defs{$found}{"${name}_MSGCNT"}++; $defs{$found}{"${name}_TIME"} = TimeNow(); } return $duplicate{$idx}{FND}; } my @found; my $cl = $hash->{Clients}; $cl = $iohash->{Clients} if(!$cl); foreach my $m (sort { $modules{$a}{ORDER} cmp $modules{$b}{ORDER} } grep {defined($modules{$_}{ORDER})} keys %modules) { next if(!(defined($cl) && $cl =~ m/:$m:/)); # Module is not loaded or the message is not for this module next if(!$modules{$m}{Match} || $dmsg !~ m/$modules{$m}{Match}/i); no strict "refs"; @found = &{$modules{$m}{ParseFn}}($hash,$dmsg); use strict "refs"; last if(int(@found)); } if(!int(@found)) { my $h = $hash->{MatchList}; $h = $iohash->{MatchList} if(!$h); if(defined($h)) { foreach my $m (sort keys %{$h}) { if($dmsg =~ m/$h->{$m}/) { my ($order, $mname) = split(":", $m); if($attr{global}{autoload_undefined_devices}) { $mname = LoadModule($mname); if($modules{$mname} && $modules{$mname}{ParseFn}) { no strict "refs"; @found = &{$modules{$mname}{ParseFn}}($hash,$dmsg); use strict "refs"; } else { Log 0, "ERROR: Cannot autoload $mname"; } } else { Log GetLogLevel($name,3), "$name: Unknown $mname device detected, " . "define one to get detailed information."; return undef; } } } } if(!int(@found)) { Log GetLogLevel($name,3), "$name: Unknown code $dmsg, help me!"; return undef; } } ################ # Inform raw if(!$iohash->{noRawInform}) { foreach my $c (keys %inform) { if(!$defs{$c} || $defs{$c}{NR} != $inform{$c}{NR}) { delete($inform{$c}); next; } next if($inform{$c}{type} ne "raw"); syswrite($defs{$c}{CD}, "$hash->{TYPE} $name $dmsg\n"); } } return undef if($found[0] eq ""); # Special return: Do not notify foreach my $found (@found) { if($found =~ m/^(UNDEFINED.*)/) { DoTrigger("global", $1); return undef; } else { if($defs{$found}) { $defs{$found}{MSGCNT}++; my $avtrigger = ($attr{$name} && $attr{$name}{addvaltrigger}); if($addvals) { foreach my $av (keys %{$addvals}) { $defs{$found}{"${name}_$av"} = $addvals->{$av}; push(@{$defs{$found}{CHANGED}}, "$av: $addvals->{$av}") if($avtrigger); } } $defs{$found}{"${name}_MSGCNT"}++; $defs{$found}{"${name}_TIME"} = TimeNow(); $defs{$found}{LASTIODev} = $name; } DoTrigger($found, undef); } } $duplicate{$idx}{FND} = \@found; return \@found; } sub CheckDuplicate($$) { my ($ioname, $msg) = @_; # Store only the "relevant" part, as the CUL won't compute the checksum $msg = substr($msg, 8) if($msg =~ m/^81/ && length($msg) > 8); my $now = gettimeofday(); my $lim = $now-AttrVal("global","dupTimeout", 0.5); foreach my $oidx (keys %duplicate) { if($duplicate{$oidx}{TIM} < $lim) { delete($duplicate{$oidx}); } elsif($duplicate{$oidx}{MSG} eq $msg && $duplicate{$oidx}{ION} ne $ioname) { return (1, $oidx); } } $duplicate{$duplidx}{ION} = $ioname; $duplicate{$duplidx}{MSG} = $msg; $duplicate{$duplidx}{TIM} = $now; $duplidx++; return (0, $duplidx-1); } sub AddDuplicate($$) { $duplicate{$duplidx}{ION} = shift; $duplicate{$duplidx}{MSG} = shift; $duplicate{$duplidx}{TIM} = gettimeofday(); $duplidx++; } sub SecondsTillTomorrow($) # 86400, if tomorrow is no DST change { my $t = shift; my $day = int($t/86400); if(!$stt_day || $day != $stt_day) { my $t = $day*86400+12*3600; my @l1 = localtime($t); my @l2 = localtime($t+86400); $stt_sec = 86400+ ($l1[2]-$l2[2])*3600+ ($l1[1]-$l2[1])*60; $stt_day = $day; } return $stt_sec; } # Add an attribute to the userattr list, if not yet present sub addToAttrList($) { my $arg = shift; my $ua = ""; $ua = $attr{global}{userattr} if($attr{global}{userattr}); my @al = split(" ", $ua); my %hash; foreach my $a (@al) { $hash{$a} = 1 if(" $AttrList " !~ m/ $a /); # Cleanse old ones } $hash{$arg} = 1 if(" $AttrList " !~ m/ $arg /); $attr{global}{userattr} = join(" ", sort keys %hash); } sub EventMapAsList($) { my ($em) = @_; my $sc = " "; # Split character my $fc = substr($em, 0, 1); # First character of the eventMap if($fc eq "," || $fc eq "/") { $sc = $fc; $em = substr($em, 1); } return split($sc, $em); } ####################### # $dir: 0 = User to Fhem (i.e. set), 1 = Fhem to User (i.e trigger) sub ReplaceEventMap($$$) { my ($dev, $str, $dir) = @_; my $em = $attr{$dev}{eventMap}; return $str if(!$em || (!$dir && $str->[1] eq "?")); my $dname = shift @{$str} if(!$dir); my $nstr = join(" ", @{$str}) if(!$dir); my $changed; my @emList = EventMapAsList($em); foreach my $rv (@emList) { my ($re, $val) = split(":", $rv, 2); # Real-Event-Regexp:GivenName next if(!defined($val)); if($dir) { # event -> GivenName if($str =~ m/$re/) { $str =~ s/$re/$val/; $changed = 1; last; } } else { # GivenName -> set command if($nstr =~ m/\b$val\b/) { $nstr =~ s/\b$val\b/$re/; $changed = 1; last; } } } return $str if($dir); if($changed) { my @arr = split(" ",$nstr); unshift @arr, $dname; return @arr; } else { unshift @{$str}, $dname; return @{$str}; } } sub setGlobalAttrBeforeFork($) { my ($f) = @_; open(FH, $f) || die("Cant open $f: $!\n"); while(my $l = ) { $l =~ s/[\r\n]//g; next if($l !~ m/^attr\s+global\s+([^\s]+)\s+(.*)$/); my ($n,$v) = ($1,$2); $v =~ s/#.*//; $v =~ s/ .*$//; $attr{global}{$n} = $v; } close(FH); } ########################################### # Functions used to make fhem-oneliners more readable, # but also recommended to be used by modules sub ReadingsVal($$$) { my ($d,$n,$default) = @_; if(defined($defs{$d}) && defined($defs{$d}{READINGS}) && defined($defs{$d}{READINGS}{$n}) && defined($defs{$d}{READINGS}{$n}{VAL})) { return $defs{$d}{READINGS}{$n}{VAL}; } return $default; } sub ReadingsTimestamp($$$) { my ($d,$n,$default) = @_; if(defined($defs{$d}) && defined($defs{$d}{READINGS}) && defined($defs{$d}{READINGS}{$n}) && defined($defs{$d}{READINGS}{$n}{TIME})) { return $defs{$d}{READINGS}{$n}{TIME}; } return $default; } sub Value($) { my ($d) = @_; if(defined($defs{$d}) && defined($defs{$d}{STATE})) { return $defs{$d}{STATE}; } return ""; } sub OldValue($) { my ($d) = @_; return $oldvalue{$d}{VAL} if(defined($oldvalue{$d})) ; return ""; } sub OldTimestamp($) { my ($d) = @_; return $oldvalue{$d}{TIME} if(defined($oldvalue{$d})) ; return ""; } sub AttrVal($$$) { my ($d,$n,$default) = @_; return $attr{$d}{$n} if($d && defined($attr{$d}) && defined($attr{$d}{$n})); return $default; } ################################################################ # Functions used by modules. sub setReadingsVal($$$$) { my ($hash,$rname,$val,$ts) = @_; $hash->{READINGS}{$rname}{VAL} = $val; $hash->{READINGS}{$rname}{TIME} = $ts; } sub addEvent($$) { my ($hash,$event) = @_; push(@{$hash->{CHANGED}}, $event); } ################################################################ # # Meta-information for devices # This part maintained by Boris Neubert omega at online dot de # ################################################################ sub Debug($) { my $msg= shift; Log 1, "DEBUG>" . $msg; } # get the names of interfaces for the device represented by the $hash # empty list is returned if interfaces are not defined sub getInterfaces($) { my ($hash)= @_; #Debug "getInterfaces(" . $hash->{NAME} .")= " . $hash->{internals}{interfaces}; if(defined($hash->{internals}{interfaces})) { return split(/:/, $hash->{internals}{interfaces}); } else { return (); } } # get the names of the setters for a named interface # empty list is returned if interface is not defined sub getSettersForInterface($) { my $interface= shift; if(defined($interface)) { return split /:/, $interfaces{$interface}{setters}; } else { return (); } } # get the names of the getters for a named interface # empty list is returned if interface is not defined sub getGettersForInterface($) { my $interface= shift; if(defined($interface)) { return split /:/, $interfaces{$interface}{getters}; } else { return (); } } # get the names of the readings for a named interface # empty list is returned if interface is not defined sub getReadingsForInterface($) { my $interface= shift; if(defined($interface)) { return split /:/, $interfaces{$interface}{readings}; } else { return (); } } # get the names of the setters for the device represented by the $hash # empty list is returned if interfaces are not defined sub getSetters($) { my ($hash)= @_; my ($interface, @setters); #Debug "getSetters..."; foreach $interface (getInterfaces($hash)) { #Debug "Interface $interface"; push @setters, getSettersForInterface($interface); } return @setters; } # get the names of the getters for the device represented by the $hash # empty list is returned if interfaces are not defined sub getGetters($) { my ($hash)= @_; my @getters; my $interface; foreach $interface (getInterfaces($hash)) { push @getters, getGettersForInterface($interface); } return @getters; } sub concatc($$$) { my ($separator,$a,$b)= @_;; return($a && $b ? $a . $separator . $b : $a . $b); } # this creates the standard interface definitions as in # http://fhemwiki.de/wiki/DevelopmentInterfaces sub createInterfaceDefinitions() { #Log 2, "Creating interface definitions..."; # The interfaces list below consists of lines with the # pipe-separated parts # - name # - ancestor # - colon separated list of readings # - colon-separated list of getters # - colon-separated list of setters # If no getters are listed they are considered identical # to the readings. # Ancestors must be listed before descendants. # Two interfaces can share a subset of readings, getters and setters # if and only if one interface is the ancestor of the other. my $IDefs= <{helper}{updating}{latestUpdate}= TimeNow(); $hash->{CHANGED}= (); return $hash->{helper}{updating}{latestUpdate}; } # # Call readingsEndUpdate when you are done updating readings. # This optionally calls DoTrigger to propagate the changes. # Rule: DoTrigger should only be called when the sub that # changes the readings is not called by Dispatch. # sub readingsEndUpdate($$) { my ($hash,$dotrigger)= @_; # turn off updating mode delete $hash->{helper}{updating}; # propagate changes if($dotrigger) { DoTrigger($hash->{NAME}, undef) if($init_done); } return undef; } # # Call readingsUpdate to update the reading. # Example: readingsUpdate($hash,"temperature",$value); # sub readingsUpdate($$$) { my ($hash,$reading,$value)= @_; my $name= $hash->{NAME}; # sanity check defined($hash->{helper}{updating}) || die "fhem.pl: readingsUpdateReading: you must call readingsBeginUpdate first."; # shorthand my $readings= $hash->{READINGS}; # these flags determine if any of the "event-on" attributes are set; my $attreocr= AttrVal($name, "event-on-change-reading", ""); my $attreour= AttrVal($name, "event-on-update-reading", ""); # these flags determine whether the reading is listed in any of the attributes my $eocr= $attreocr && grep($_ eq $reading, split /,/,$attreocr); my $eour= $attreour && grep($_ eq $reading, split /,/,$attreour); # determine if an event should be created my $changed= !($attreocr || $attreour) # always create event if no attribute is set || $eour # or if the reading is listed in event-on-update-reading || ($eocr && # or if the reading is listed in event-on-change-reading... ($value ne $readings->{$reading}{VAL})); # ...and its value has changed. setReadingsVal($hash, $reading, $value, $hash->{helper}{updating}{latestUpdate}); # add to CHANGED hash #Log 1, "changed!" if($changed); # DEBUG my $rv= "$reading: $value"; addEvent($hash, $rv) if($changed); return $rv; } ############################################################################### # # date and time routines # ############################################################################## sub fhemTzOffset($) { # see http://stackoverflow.com/questions/2143528/whats-the-best-way-to-get-the-utc-offset-in-perl my $t = shift; my @l = localtime($t); my @g = gmtime($t); # the offset is positive if the local timezone is ahead of GMT, e.g. we get 2*3600 seconds for CET DST vs GMT return 60*(($l[2] - $g[2] + ((($l[5]<<9)|$l[7]) <=> (($g[5]<<9)|$g[7])) * 24) * 60 + $l[1] - $g[1]); } sub fhemTimeGm($$$$$$) { # see http://de.wikipedia.org/wiki/Unixzeit my ($sec,$min,$hour,$mday,$month,$year) = @_; # $mday= 1.. # $month= 0..11 # $year is year-1900 $year+= 1900; my $isleapyear= $year % 4 ? 0 : $year % 100 ? 1 : $year % 400 ? 0 : 1; my $leapyears= int((($year-1)-1968)/4 - (($year-1)-1900)/100 + (($year-1)-1600)/400); #Debug sprintf("%02d.%02d.%04d %02d:%02d:%02d", $mday,$month+1,$year,$hour,$min,$sec); if ( $^O eq 'MacOS' ) { $year-= 1904; } else { $year-= 1970; # the Unix Epoch } my @d= (0,31,59,90,120,151,181,212,243,273,304,334); # no leap day # add one day in leap years if month is later than February $mday++ if($month>1 && $isleapyear); return $sec+60*($min+60*($hour+24*($d[$month]+$mday-1+365*$year+$leapyears))); } sub fhemTimeLocal($$$$$$) { my $t= fhemTimeGm($_[0],$_[1],$_[2],$_[3],$_[4],$_[5]); return $t-fhemTzOffset($t); } 1;