#!/usr/bin/perl my $version = "=VERS= from =DATE="; ################################################################ # # Copyright notice # # (c) 2005 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://www.koeniglich.de/fhem/fhem.html use strict; use warnings; use IO::File; use IO::Socket; use Net::hostent; use Time::HiRes qw(gettimeofday); ################################################## # Forward declarations # sub AnalyzeInput($); sub AnalyzeCommand($$); sub AnalyzeCommandChain($$); sub IOWrite($@); sub AssignIoPort($); sub InternalTimer($$$); sub fhz($); sub CommandChain($$); sub DoClose($); sub HandleTimeout(); sub Log($$); sub OpenLogfile($); sub ResolveDateWildcards($@); sub SignalHandling(); sub TimeNow(); sub DoSavefile(); sub SemicolonEscape($); sub XmlEscape($); sub CommandAt($$); sub CommandAttr($$); sub CommandDefAttr($$); sub CommandDefine($$); sub CommandDelete($$); sub CommandFhzDev($$); sub CommandGet($$); sub CommandHelp($$); sub CommandInclude($$); sub CommandInform($$); sub CommandList($$); sub CommandLogfile($$); sub CommandModpath($$); sub CommandNotifyon($$); sub CommandPidfile($$); sub CommandPort($$); sub CommandRereadCfg($$); sub CommandQuit($$); sub CommandSavefile($$); sub CommandSet($$); sub CommandSetstate($$); sub CommandSleep($$); sub CommandShutdown($$); sub CommandVerbose($$); sub CommandXmlList($$); sub CommandTrigger($$); ################################################## # Variables: # global, to be able to access them from modules use vars qw(%defs); # FHEM device/button definitions use vars qw(%logs); # Log channels use vars qw(%attr); # Attributes use vars qw(%value); # Current values, see commandref.html use vars qw(%oldvalue); # Old values, see commandref.html use vars qw(%devmods); # List of loaded device modules my %ntfy; my %at; my $server; # Server socket my $verbose = 0; my $logfile; # logfile name, if its "-" then wont background my $currlogfile; # logfile, without wildcards my $logopened; my %client; # Client array my %logmods; # List of loaded logger modules my $savefile = ""; # Save ste info and at cmd's here my $nextat; my $rcvdquit; # Used for quit handling in init files my $configfile=$ARGV[0]; my $sig_term = 0; # if set to 1, terminate (saving the state) my $modpath_set; # Check if modpath was used, and report if not. my $global_cl; # To use from perl snippets my $devcount = 0; # To sort the devices my %defattr; # Default attributes my %intAt; # Internal at timer hash. my $intAtCnt=0; my $init_done = 0; my $pidfilename; my %cmds = ( "?" => { Fn=>"CommandHelp", Hlp=>",get this help" }, "at" => { Fn=>"CommandAt", Hlp=>" ,issue a command at a given time" }, "attr" => { Fn=>"CommandAttr", Hlp=>" ,set attributes for " }, "defattr" => { Fn=>"CommandDefAttr", Hlp=>" ,set attr for following definitions" }, "define" => { Fn=>"CommandDefine", Hlp=>" ,define a code" }, "delete" => { Fn=>"CommandDelete", Hlp=>"{def|ntfy|at} name,delete the corresponding definition"}, "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|off},echo all commands and events to this client" }, "list" => { Fn=>"CommandList", Hlp=>"[device],list definitions and status info" }, "logfile" => { Fn=>"CommandLogfile", Hlp=>"filename,use - for stdout" }, "modpath" => { Fn=>"CommandModpath", Hlp=>",the directory where the FHEM subdir is" }, "notifyon"=> { Fn=>"CommandNotifyon", Hlp=>" ,exec when recvd signal for " }, "pidfile" => { Fn=>"CommandPidfile", Hlp=>"filename,write the process id into the pidfile" }, "port" => { Fn=>"CommandPort", Hlp=>" [global],TCP/IP port for the server" }, "quit" => { Fn=>"CommandQuit", Hlp=>",end the client session" }, "reload" => { Fn=>"CommandReload", Hlp=>",reload the given module (e.g. 99_PRIV)" }, "rereadcfg" => { Fn=>"CommandRereadCfg", Hlp=>",reread the config file" }, "savefile"=> { Fn=>"CommandSavefile", Hlp=>",on shutdown save all states and at entries" }, "set" => { Fn=>"CommandSet", Hlp=>" ,transmit code for " }, "setstate"=> { Fn=>"CommandSetstate", Hlp=>" ,set the state shown in the command list" }, "shutdown"=> { Fn=>"CommandShutdown", Hlp=>",terminate the server" }, "sleep" => { Fn=>"CommandSleep", Hlp=>",sleep for usecs" }, "trigger" => { Fn=>"CommandTrigger", Hlp=>" ,trigger notify command" }, "verbose" => { Fn=>"CommandVerbose", Hlp=>",verbosity level, 0-5" }, "xmllist" => { Fn=>"CommandXmlList", Hlp=>",list definitions and status info as xml" }, ); ################################################### # 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); } ################################################### # Client code if(int(@ARGV) == 2) { my $buf; my $addr = $ARGV[0]; $addr = "localhost:$addr" if($ARGV[0] !~ m/:/); $server = IO::Socket::INET->new(PeerAddr => $addr); die "Can't connect to $addr\n" if(!$server); syswrite($server, "$ARGV[1] ; quit\n"); my $err = 0; while(sysread($server, $buf, 256) > 0) { print($buf); $err = 1; } exit($err); } my $ret = CommandInclude(undef, $configfile); die($ret) if($ret); if($logfile ne "-") { defined(my $pid = fork) || die "Can't fork: $!"; exit(0) if $pid; } die("No modpath specified in the configfile.\n") if(!$modpath_set); if($savefile && -r $savefile) { $ret = CommandInclude(undef, $savefile); die($ret) if($ret); } SignalHandling(); Log 0, "Server started (version $version, pid $$)"; ################################################ # Main loop $init_done = 1; CommandPidfile(undef, $pidfilename) if($pidfilename); while (1) { my ($rout, $rin) = ('', ''); vec($rin, $server->fileno(), 1) = 1; foreach my $p (keys %defs) { vec($rin, $defs{$p}{FD}, 1) = 1 if($defs{$p}{FD}); } foreach my $c (keys %client) { vec($rin, fileno($client{$c}{fd}), 1) = 1; } my $nfound = select($rout=$rin, undef, undef, HandleTimeout()); CommandShutdown(undef, undef) if($sig_term); if($nfound < 0) { next if ($! == EAGAIN() || $! == EINTR() || $! == 0); die("Select error $nfound / $!\n"); } ############################### # Message from the hardware (FHZ1000/WS3000/etc) foreach my $p (keys %defs) { next if(!$defs{$p}{FD} || !vec($rout, $defs{$p}{FD}, 1)); no strict "refs"; &{$devmods{$defs{$p}{TYPE}}{ReadFn}}($defs{$p}); use strict "refs"; } if(vec($rout, $server->fileno(), 1)) { my @clientinfo = $server->accept(); if(!@clientinfo) { Print("ERROR", 1, "016 Accept failed for admin port"); next; } my @clientsock = sockaddr_in($clientinfo[1]); my $fd = $clientinfo[0]; $client{$fd}{fd} = $fd; $client{$fd}{addr} = inet_ntoa($clientsock[1]) . ":" . $clientsock[0]; $client{$fd}{buffer} = ""; Log 4, "Connection accepted from $client{$fd}{addr}"; } foreach my $c (keys %client) { next unless (vec($rout, fileno($client{$c}{fd}), 1)); my $buf; my $ret = sysread($client{$c}{fd}, $buf, 256); if(!defined($ret) || $ret <= 0) { DoClose($c); next; } if(ord($buf) == 4) { # EOT / ^D CommandQuit($c, ""); next; } $buf =~ s/\r//g; $client{$c}{buffer} .= $buf; AnalyzeInput($c); } } ################################################ sub IsDummy($) { my $dev = shift; return 1 if(defined($attr{$dev}) && defined($attr{$dev}{dummy})); return 0; } ################################################ sub GetLogLevel($) { my $dev = shift; return $attr{$dev}{loglevel} if(defined($attr{$dev}) && defined($attr{$dev}{loglevel})); return 2; } ################################################ sub Log($$) { my ($loglevel, $text) = @_; return if($loglevel > $verbose); my @t = localtime; my $nfile = ResolveDateWildcards($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]); # my ($seconds, $microseconds) = gettimeofday(); # $tim = sprintf("%04d.%02d.%02d %02d:%02d:%02d.%03d", # $t[5]+1900,$t[4]+1,$t[3], $t[2],$t[1],$t[0], $microseconds/1000); if($logopened) { print LOG "$tim $loglevel: $text\n"; } else { print "$tim $loglevel: $text\n"; } return undef; } ##################################### sub DoClose($) { my $c = shift; Log 4, "Connection closed for $client{$c}{addr}"; close($client{$c}{fd}); delete($client{$c}); return undef; } ##################################### sub IOWrite($@) { my ($hash, @a) = @_; my $iohash = $hash->{IODev}; if(!$iohash) { Log 5, "No IO device found for $hash->{NAME}"; return; } no strict "refs"; &{$devmods{$iohash->{TYPE}}{WriteFn}}($iohash, @a); use strict "refs"; } ##################################### sub AnalyzeInput($) { my $c = shift; while($client{$c}{buffer} =~ m/\n/) { my ($cmd, $rest) = split("\n", $client{$c}{buffer}, 2); $client{$c}{buffer} = $rest; if($cmd) { AnalyzeCommandChain($c, $cmd); return if(!defined($client{$c})); # quit } else { $client{$c}{prompt} = 1; } syswrite($client{$c}{fd}, "FHZ> ") if($client{$c}{prompt} && $rest !~ m/\n/); } } ##################################### sub AnalyzeCommandChain($$) { my ($c, $cmd) = @_; $cmd =~ s/#.*$//; $cmd =~ s/;;/____/g; foreach my $subcmd (split(";", $cmd)) { $subcmd =~ s/____/;/g; AnalyzeCommand($c, $subcmd); last if($c && !defined($client{$c})); # quit } } ##################################### # Used from perl oneliners inside of scripts sub fhz($) { my $param = shift; return AnalyzeCommandChain($global_cl, $param); } ##################################### sub AnalyzeCommand($$) { my ($cl, $cmd) = @_; $cmd =~ s/^[ \t]*//; # Strip space $cmd =~ s/[ \t]*$//; Log 5, "Cmd: >$cmd<"; return if(!$cmd); if($cmd =~ m/^{.*}$/) { # Perl code # 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); $month++; $year+=1900; $global_cl = $cl; my $ret = eval $cmd; $ret = $@ if($@); if($ret) { if($cl) { syswrite($client{$cl}{fd}, "$ret\n") } else { Log 3, $ret; } } return $ret; } if($cmd =~ m/^"(.*)"$/) { # Shell code, always in bg system("$1 &"); return; } $cmd =~ s/^[ \t]*//; my ($fn, $param) = split("[ \t][ \t]*", $cmd, 2); return if(!$fn); ############# # 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; } } } if(!defined($cmds{$fn})) { if($cl) { syswrite($client{$cl}{fd}, "Unknown command $fn, try help\n"); } else { Log 1, "Unknown command >$fn<, try help"; } return; } $param = "" if(!defined($param)); no strict "refs"; my $ret = &{$cmds{$fn}{Fn} }($cl, $param); use strict "refs"; if($ret) { if($cl) { syswrite($client{$cl}{fd}, $ret . "\n"); } else { Log 1, $ret; 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) = @_; if(!open(CFG, $arg)) { return "Can't open $arg: $!"; } my $bigcmd = ""; $rcvdquit = 0; while(my $l = ) { chomp($l); if($l =~ m/^(.*)\\$/) { # Multiline commands $bigcmd .= $1; } else { AnalyzeCommandChain($cl, $bigcmd . $l); $bigcmd = ""; } last if($rcvdquit); } close(CFG); return undef; } ##################################### sub CommandPort($$) { my ($cl, $arg) = @_; my ($port, $global) = split(" ", $arg); if($global && $global ne "global") { return "Bad syntax, usage: port [global]"; } close($server) if($server); $server = IO::Socket::INET->new( Proto => 'tcp', LocalHost => ($global ? undef : "localhost"), LocalPort => $port, Listen => 10, ReuseAddr => 1); die "Can't open server port at $port\n" if(!$server); return undef; } ##################################### sub OpenLogfile($) { my $param = shift; close(LOG) if($logfile); $logopened=0; $currlogfile = $param; if($currlogfile eq "-") { open LOG, '>&STDOUT' or die "Can't dup stdout: $!"; } else { open(LOG, ">>$currlogfile") || return("Can't open $currlogfile: $!"); # Redirect stdin/stderr open STDIN, '>$currlogfile") or return "Can't append STDERR to log: $!"; STDERR->autoflush(1); close(STDOUT); open STDOUT, '>&STDERR' or return "Can't dup stdout: $!"; STDOUT->autoflush(1); } LOG->autoflush(1); $logopened = 1; return undef; } ##################################### sub CommandLogfile($$) { my ($cl, $param) = @_; $logfile = $param; my @t = localtime; my $ret = OpenLogfile(ResolveDateWildcards($param, @t)); die($ret) if($ret); return undef; } ##################################### sub CommandVerbose($$) { my ($cl, $param) = @_; if($param =~ m/^[0-5]$/) { $verbose = $param; return undef; } else { return "Valid value for verbose are 0,1,2,3,4,5"; } } ##################################### sub CommandRereadCfg($$) { my ($cl, $param) = @_; return "RereadCfg: No parameters are accepted" if($param); DoSavefile(); foreach my $d (keys %defs) { no strict "refs"; my $ret = &{$devmods{$defs{$d}{TYPE}}{UndefFn}}($defs{$d}, $d); use strict "refs"; return $ret if($ret); } %defs = (); %logs = (); %attr = (); %ntfy = (); %at = (); my $ret; $ret = CommandInclude($cl, $configfile); return $ret if($ret); $ret = CommandInclude($cl, $savefile) if($savefile); return $ret; } ##################################### sub CommandQuit($$) { my ($cl, $param) = @_; if(!$cl) { $rcvdquit = 1; return; } syswrite($client{$cl}{fd}, "Bye...\n") if($client{$cl}{prompt}); DoClose($cl); return undef; } ##################################### sub DoSavefile() { return if(!$savefile); if(!open(SFH, ">$savefile")) { Log 1, "Cannot open $savefile: $!"; return; } my $t = localtime; print SFH "#$t\n"; foreach my $d (sort keys %defs) { my $t = $defs{$d}{TYPE}; print SFH "setstate $d $defs{$d}{STATE}\n" if($defs{$d}{STATE} && $defs{$d}{STATE} ne "unknown"); ############# # Now the detailed list no strict "refs"; my $str = &{$devmods{$defs{$d}{TYPE}}{ListFn}}($defs{$d}); use strict "refs"; next if($str =~ m/^No information about/); foreach my $l (split("\n", $str)) { print SFH "setstate $d $l\n" } } foreach my $t (sort keys %at) { # $t =~ s/_/ /g; # Why is this here? print SFH "at $t\n"; } close(SFH); } ##################################### sub CommandShutdown($$) { my ($cl, $param) = @_; Log 0, "Server shutdown"; DoSavefile(); unlink($pidfilename) if($pidfilename); exit(0); } ##################################### sub CommandNotifyon($$) { my ($cl, $param) = @_; my @a = split("[ \t]", $param, 2); # Checking for misleading regexps eval { "Hallo" =~ m/^$a[0]$/ }; return "Bad regexp: $@" if($@); $ntfy{$a[0]} = SemicolonEscape($a[1]); return undef; } ##################################### sub DoSet(@) { my @a = @_; my $dev = $a[0]; my $ret; no strict "refs"; $ret = &{$devmods{$defs{$dev}{TYPE}}{SetFn}}($defs{$dev}, @a); use strict "refs"; return $ret if($ret); shift @a; return DoTrigger($dev, join(" ", @a)); } ##################################### sub CommandSet($$) { my ($cl, $param) = @_; my @a = split("[ \t][ \t]*", $param); return "Usage: set " if(int(@a) < 1); my $dev = $a[0]; my @rets; if(defined($defs{$dev})) { return DoSet(@a); } elsif($dev =~ m/,/) { # Enumeration (separated by ,) foreach my $sdev (split(",", $dev)) { push @rets, "Please define $sdev first" if(!defined($defs{$sdev})); $a[0] = $sdev; my $ret = DoSet(@a); push @rets, $ret if($ret); } return join("\n", @rets); } elsif($dev =~ m/-/) { # Range (separated by -) my @lim = split("-", $dev); foreach my $sdev (keys %defs) { next if($sdev lt $lim[0] || $sdev gt $lim[1]); $a[0] = $sdev; my $ret = DoSet(@a); push @rets, $ret if($ret); } return join("\n", @rets); } else { return "Please define $dev first ($param)"; } } ##################################### sub CommandGet($$) { my ($cl, $param) = @_; my @a = split("[ \t][ \t]*", $param); return "Usage: get " if(int(@a) < 1); my $dev = $a[0]; return "Please define $dev first ($param)" if(!defined($defs{$dev})); ######################## # Type specific set my $ret; no strict "refs"; $ret = &{$devmods{$defs{$a[0]}{TYPE}}{GetFn}}($defs{$dev}, @a); use strict "refs"; return $ret; } ##################################### 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 = eval $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 must return a timespec HH:MM:SS 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); } ##################################### sub CommandAt($$) { my ($cl, $def) = @_; my ($tm, $command) = split("[ \t]+", $def, 2); return "Usage: at " if(!$command); return "Wrong timespec, use \"[+][*[{count}]]