diff --git a/fhem/CHANGED b/fhem/CHANGED index bf9e68c80..35e50937f 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -13,6 +13,8 @@ - feature: attr may be a regexp (for CUL_IR) - feature: Homepage moved from koeniglich.de/fhem to fhem.de - feature: eventMap attribute + - feature: new modules 66_ECMD.pm and 67_ECMDDevice.pm for ethersex-enabled + devices and alike. - 2010-08-15 (5.0) - **NOTE*: The default installation path is changed to satisfy lintian diff --git a/fhem/FHEM/66_ECMD.pm b/fhem/FHEM/66_ECMD.pm new file mode 100644 index 000000000..0bcb0dee6 --- /dev/null +++ b/fhem/FHEM/66_ECMD.pm @@ -0,0 +1,480 @@ +# +# +# 66_ECMD.pm +# written by Dr. Boris Neubert 2011-01-15 +# e-mail: omega at online dot de +# +############################################## +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); + + +#sub ECMD_Attr(@); +sub ECMD_Clear($); +#sub ECMD_Parse($$$$$); +#sub ECMD_Read($); +sub ECMD_ReadAnswer($$); +#sub ECMD_Ready($); +sub ECMD_Write($$); + +sub ECMD_OpenDev($$); +sub ECMD_CloseDev($); +sub ECMD_SimpleWrite(@); +sub ECMD_SimpleRead($); +sub ECMD_Disconnected($); + +use vars qw {%attr %defs}; + +##################################### +sub +ECMD_Initialize($) +{ + my ($hash) = @_; + +# Provider + $hash->{WriteFn} = "ECMD_Write"; + #$hash->{ReadFn} = "ECMD_Read"; + $hash->{Clients}= ":ECMDDevice:"; + +# Consumer + $hash->{DefFn} = "ECMD_Define"; + $hash->{UndefFn} = "ECMD_Undef"; + $hash->{GetFn} = "ECMD_Get"; + $hash->{SetFn} = "ECMD_Set"; + $hash->{AttrList}= "loglevel:0,1,2,3,4,5"; +} + +##################################### +sub +ECMD_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + + my $name = $a[0]; + my $protocol = $a[2]; + my $ipaddress= $a[3]; + + if(@a < 4 || @a > 4 || $protocol ne "telnet") { + my $msg = "wrong syntax: define ECMD telnet "; + Log 2, $msg; + return $msg; + } + + ECMD_CloseDev($hash); + + if($ipaddress eq "none") { + Log 1, "$name ip address is none, commands will be echoed only"; + $attr{$name}{dummy} = 1; + return undef; + } + + $hash->{Protocol}= $protocol; + $hash->{IPAddress}= $ipaddress; + + my $ret = ECMD_OpenDev($hash, 0); + return $ret; +} + + +##################################### +sub +ECMD_Undef($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + foreach my $d (sort keys %defs) { + if(defined($defs{$d}) && + defined($defs{$d}{IODev}) && + $defs{$d}{IODev} == $hash) + { + my $lev = ($reread_active ? 4 : 2); + Log GetLogLevel($name,$lev), "deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + + ECMD_CloseDev($hash); + return undef; +} + +##################################### +sub +ECMD_CloseDev($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $ipaddress = $hash->{IPAddress}; + + return if(!$ipaddress); + + if($hash->{TCPDev}) { + $hash->{TCPDev}->close(); + delete($hash->{TCPDev}); + } + + delete($selectlist{"$name.$ipaddress"}); + delete($readyfnlist{"$name.$ipaddress"}); + delete($hash->{FD}); +} + +######################## +sub +ECMD_OpenDev($$) +{ + my ($hash, $reopen) = @_; + my $protocol = $hash->{Protocol}; + my $ipaddress = $hash->{IPAddress}; + my $name = $hash->{NAME}; + + + $hash->{PARTIAL} = ""; + Log 3, "ECMD opening $name (protocol $protocol, ipaddress $ipaddress)" + if(!$reopen); + + # This part is called every time the timeout (5sec) is expired _OR_ + # somebody is communicating over another TCP connection. As the connect + # for non-existent devices has a delay of 3 sec, we are sitting all the + # time in this connect. NEXT_OPEN tries to avoid this problem. + if($hash->{NEXT_OPEN} && time() < $hash->{NEXT_OPEN}) { + return; + } + + my $conn = IO::Socket::INET->new(PeerAddr => $ipaddress); + if($conn) { + delete($hash->{NEXT_OPEN}) + } else { + Log(3, "Can't connect to $ipaddress: $!") if(!$reopen); + $readyfnlist{"$name.$ipaddress"} = $hash; + $hash->{STATE} = "disconnected"; + $hash->{NEXT_OPEN} = time()+60; + return ""; + } + + $hash->{TCPDev} = $conn; + $hash->{FD} = $conn->fileno(); + delete($readyfnlist{"$name.$ipaddress"}); + $selectlist{"$name.$ipaddress"} = $hash; + + + if($reopen) { + Log 1, "ECMD $ipaddress reappeared ($name)"; + } else { + Log 3, "ECMD device opened"; + } + + $hash->{STATE}= ""; # Allow InitDev to set the state + my $ret = ECMD_DoInit($hash); + + if($ret) { + Log 1, "$ret"; + ECMD_CloseDev($hash); + Log 1, "Cannot init $ipaddress, ignoring it"; + } + + DoTrigger($name, "CONNECTED") if($reopen); + return $ret; +} + +##################################### +sub +ECMD_DoInit($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + my $msg = undef; + + ECMD_Clear($hash); + ECMD_SimpleWrite($hash, "version"); + my ($err,$version)= ECMD_ReadAnswer($hash, "version"); + return "$name: $err" if($err); + Log 2, "ECMD version: $version"; + + $hash->{VERSION} = $version; + + #ECMD_SimpleWrite($hash, $hash->{initString}); + + $hash->{STATE} = "Initialized" if(!$hash->{STATE}); + + return undef; +} + +######################## +sub +ECMD_SimpleWrite(@) +{ + my ($hash, $msg, $nonl) = @_; + return if(!$hash); + + $msg .= "\n" unless($nonl); + syswrite($hash->{TCPDev}, $msg) if($hash->{TCPDev}); + + select(undef, undef, undef, 0.001); +} + +######################## +sub +ECMD_SimpleRead($) +{ + my ($hash) = @_; + + if($hash->{TCPDev}) { + my $buf; + if(!defined(sysread($hash->{TCPDev}, $buf, 256))) { + ECMD_Disconnected($hash); + return undef; + } + + return $buf; + } + return undef; +} + +##################################### +# This is a direct read for commands like get +sub +ECMD_ReadAnswer($$) +{ + my ($hash, $arg) = @_; + + #Log 5, "ECMD reading answer for get $arg..."; + + return ("No FD", undef) + if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); + + my ($data, $rin) = ("", ''); + my $buf; + my $to = 3; # 3 seconds timeout + $to = $hash->{RA_Timeout} if($hash->{RA_Timeout}); # ...or less + #Log 5, "Timeout is $to seconds"; + for(;;) { + + return ("Device lost when reading answer for get $arg", undef) + if(!$hash->{FD}); + + vec($rin, $hash->{FD}, 1) = 1; + my $nfound = select($rin, undef, undef, $to); + if($nfound < 0) { + next if ($! == EAGAIN() || $! == EINTR() || $! == 0); + my $err = $!; + ECMD_Disconnected($hash); + return("Error reading answer for get $arg: $err", undef); + } + return ("Timeout reading answer for get $arg", undef) + if($nfound == 0); + + $buf = ECMD_SimpleRead($hash); + return ("No data", undef) if(!defined($buf)); + + if($buf) { + chomp $buf; # remove line break + Log 5, "ECMD (ReadAnswer): $buf"; + $data .= $buf; + } + return (undef, $data) + } +} + +##################################### +sub +ECMD_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + return undef; +} + +##################################### +sub +ECMD_Clear($) +{ + my $hash = shift; + + # Clear the pipe + $hash->{RA_Timeout} = 0.1; + for(;;) { + my ($err, undef) = ECMD_ReadAnswer($hash, "clear"); + last if($err && $err =~ m/^Timeout/); + } + delete($hash->{RA_Timeout}); +} + +##################################### +sub +ECMD_Disconnected($) +{ + my $hash = shift; + my $ipaddress = $hash->{IPAddress}; + my $name = $hash->{NAME}; + + return if(!defined($hash->{FD})); # Already deleted o + + Log 1, "$ipaddress disconnected, waiting to reappear"; + ECMD_CloseDev($hash); + $readyfnlist{"$name.$ipaddress"} = $hash; # Start polling + $hash->{STATE} = "disconnected"; + + # Without the following sleep the open of the device causes a SIGSEGV, + # and following opens block infinitely. Only a reboot helps. + sleep(5); + + DoTrigger($name, "DISCONNECTED"); +} + +##################################### +sub +ECMD_Get($@) +{ + my ($hash, @a) = @_; + + return "get needs at least one parameter" if(@a < 2); + + my $name = $a[0]; + my $cmd= $a[1]; + my $arg = ($a[2] ? $a[2] : ""); + my @args= @a; shift @args; shift @args; + my ($msg, $err); + + return "No get $cmd for dummies" if(IsDummy($name)); + + if($cmd eq "raw") { + return "get raw needs an argument" if(@a< 3); + my $ecmd= join " ", @args; + Log 5, $ecmd; + ECMD_SimpleWrite($hash, $ecmd); + ($err, $msg) = ECMD_ReadAnswer($hash, "raw"); + return $err if($err); + } else { + return "get $cmd: unknown command "; + } + + $hash->{READINGS}{$cmd}{VAL} = $msg; + $hash->{READINGS}{$cmd}{TIME} = TimeNow(); + + return "$name $cmd => $msg"; +} + +##################################### +sub +ECMD_Set($@) +{ + my ($hash, @a) = @_; + my $name = $a[0]; + + # usage check + my $usage= "Usage: set $name classdef "; + return $usage if(@a != 4); + return $usage if($a[1] ne "classdef"); + + # from the definition + my $classname= $a[2]; + my $filename= $a[3]; + + # refuse overwriting existing definitions + if(defined($hash->{fhem}{classDefs}{$classname})) { + my $err= "$name: class $classname is already defined."; + Log 1, $err; + return $err; + } + + # try and open the class definition file + if(!open(CLASSDEF, $filename)) { + my $err= "$name: cannot open file $filename for class $classname."; + Log 1, $err; + return $err; + } + my @classdef= ; + close(CLASSDEF); + + # add the class definition + Log 5, "$name: adding new class $classname from file $filename"; + $hash->{fhem}{classDefs}{$classname}{filename}= $filename; + + # format of the class definition: + # params parameters for device definition + # get cmd {} defines a get command + # get params parameters for get command + # set cmd {} defines a set command + # set params parameters for get command + # all lines are optional + # + # eaxmple class definition 1: + # get adc cmd {"adc get %channel"} + # get adc params channel + # + # eaxmple class definition 1: + # params btnup btnstop btndown + # set up cmd {"io set ddr 2 ff\nio set port 2 1%btnup\nwait 1000\nio set port 2 00"} + # set stop cmd {"io set ddr 2 ff\nio set port 2 1%btnstop\nwait 1000\nio set port 2 00"} + # set down cmd {"io set ddr 2 ff\nio set port 2 1%btndown\nwait 1000\nio set port 2 00"} + + foreach my $line (@classdef) { + # kill trailing newline + chomp $line; + # kill comments and blank lines + $line=~ s/\#.*$//; + $line=~ s/\s+$//; + next unless($line); + Log 5, "$name: evaluating >$line<"; + # split line into command and definition + my ($cmd, $def)= split("[ \t]+", $line, 2); + if($cmd eq "params") { + Log 5, "$name: parameters are $def"; + $hash->{fhem}{classDefs}{$classname}{params}= $def; + } elsif($cmd eq "set" || $cmd eq "get") { + my ($cmdname, $spec, $arg)= split("[ \t]+", $def, 3); + if($spec eq "params") { + if($cmd eq "set") { + Log 5, "$name: set $cmdname has parameters $arg"; + $hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{params}= $arg; + } elsif($cmd eq "get") { + Log 5, "$name: get $cmdname has parameters $arg"; + $hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{params}= $arg; + } + } elsif($spec eq "cmd") { + if($arg !~ m/^{.*}$/s) { + Log 1, "$name: command for $cmd $cmdname is not a perl command."; + next; + } + $arg =~ s/^(\\\n|[ \t])*//; # Strip space or \\n at the begginning + $arg =~ s/[ \t]*$//; + if($cmd eq "set") { + Log 5, "$name: set $cmdname defined as $arg"; + $hash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{cmd}= $arg; + } elsif($cmd eq "get") { + Log 5, "$name: get $cmdname defined as $arg"; + $hash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{cmd}= $arg; + } + } + } else { + Log 1, "$name: illegal tag $cmd for class $classname in file $filename."; + } + } + + return undef; +} + +##################################### +sub +ECMD_Write($$) +{ + my ($hash,$msg) = @_; + my $answer; + my @r; + my @ecmds= split "\n", $msg; + foreach my $ecmd (@ecmds) { + Log 5, "$hash->{NAME} sending $ecmd"; + ECMD_SimpleWrite($hash, $ecmd); + $answer= ECMD_ReadAnswer($hash, "'ecmd"); + push @r, $answer; + Log 5, $answer; + } + return join(";", @r); +} + +##################################### + +1; diff --git a/fhem/FHEM/67_ECMDDevice.pm b/fhem/FHEM/67_ECMDDevice.pm new file mode 100644 index 000000000..f4eaf0ac0 --- /dev/null +++ b/fhem/FHEM/67_ECMDDevice.pm @@ -0,0 +1,203 @@ +# +# +# 66_ECMDDevice.pm +# written by Dr. Boris Neubert 2011-01-15 +# e-mail: omega at online dot de +# +############################################## +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); + +sub ECMDDevice_Get($@); +sub ECMDDevice_Set($@); +sub ECMDDevice_Define($$); + +my %gets= ( +); + +my %sets= ( +); + +################################### +sub +ECMDDevice_Initialize($) +{ + my ($hash) = @_; + + $hash->{GetFn} = "ECMDDevice_Get"; + $hash->{SetFn} = "ECMDDevice_Set"; + $hash->{DefFn} = "ECMDDevice_Define"; + + $hash->{AttrList} = "loglevel 0,1,2,3,4,5"; +} + +sub +ECMDDevice_AnalyzeCommand($) +{ + my ($ecmd)= @_; + Log 5, "ECMDDevice: Analyze command >$ecmd<"; + return AnalyzePerlCommand(undef, $ecmd); +} + +############################# +sub +ECMDDevice_GetDeviceParams($) +{ + my ($hash)= @_; + my $classname= $hash->{fhem}{classname}; + my $IOhash= $hash->{IODev}; + if(defined($IOhash->{fhem}{classDefs}{$classname}{params})) { + my $params= $IOhash->{fhem}{classDefs}{$classname}{params}; + return split("[ \t]+", $params); + } + return; +} + +sub +ECMDDevice_DeviceParams2Specials($) +{ + my ($hash)= @_; + my %specials= ( + "%NAME" => $hash->{NAME}, + "%TYPE" => $hash->{TYPE} + ); + my @deviceparams= ECMDDevice_GetDeviceParams($hash); + foreach my $param (@deviceparams) { + $specials{"%".$param}= $hash->{fhem}{params}{$param}; + } + return %specials; +} + + +################################### +sub +ECMDDevice_Get($@) +{ + my ($hash, @a)= @_; + + my $name= $hash->{NAME}; + my $type= $hash->{TYPE}; + return "get $name needs at least one argument" if(int(@a) < 2); + my $cmdname= $a[1]; + + my $IOhash= $hash->{IODev}; + my $classname= $hash->{fhem}{classname}; + if(!defined($IOhash->{fhem}{classDefs}{$classname}{gets}{$cmdname})) { + return "$name error: unknown command $cmdname"; + } + + my $ecmd= $IOhash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{cmd}; + my $params= $IOhash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{params}; + + my %specials= ECMDDevice_DeviceParams2Specials($hash); + # add specials for command + if($params) { + shift @a; shift @a; + my @params= split('[\s]+', $params); + return "Wrong number of parameters." if($#a != $#params); + + my $i= 0; + foreach my $param (@params) { + Log 5, "Parameter %". $param . " is " . $a[$i]; + $specials{"%".$param}= $a[$i++]; + } + } + $ecmd= EvalSpecials($ecmd, %specials); + + my $r = ECMDDevice_AnalyzeCommand($ecmd); + + my $v= IOWrite($hash, $r); + + return "$name $cmdname => $v" ; +} + + +############################# +sub +ECMDDevice_Set($@) +{ + my ($hash, @a)= @_; + + my $name= $hash->{NAME}; + my $type= $hash->{TYPE}; + return "set $name needs at least one argument" if(int(@a) < 2); + my $cmdname= $a[1]; + + my $IOhash= $hash->{IODev}; + my $classname= $hash->{fhem}{classname}; + if(!defined($IOhash->{fhem}{classDefs}{$classname}{sets}{$cmdname})) { + return "$name error: unknown command $cmdname"; + } + + my $ecmd= $IOhash->{fhem}{classDefs}{$classname}{sets}{$cmdname}{cmd}; + my $params= $IOhash->{fhem}{classDefs}{$classname}{gets}{$cmdname}{params}; + + my %specials= ECMDDevice_DeviceParams2Specials($hash); + # add specials for command + if($params) { + shift @a; shift @a; + my @params= split('[\s]+', $params); + return "Wrong number of parameters." if($#a != $#params); + + my $i= 0; + foreach my $param (@params) { + $specials{"%".$param}= $a[$i++]; + } + } + $ecmd= EvalSpecials($ecmd, %specials); + + my $r = ECMDDevice_AnalyzeCommand($ecmd); + + return IOWrite($hash, $r); +} + + +############################# + +sub +ECMDDevice_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + + return "Usage: define ECMDDevice [...]" if(int(@a) < 3); + my $name= $a[0]; + my $classname= $a[2]; + + AssignIoPort($hash); + + my $IOhash= $hash->{IODev}; + if(!defined($IOhash->{fhem}{classDefs}{$classname}{filename})) { + my $err= "$name error: unknown class $classname."; + Log 1, $err; + return $err; + } + + $hash->{fhem}{classname}= $classname; + + my @prms= ECMDDevice_GetDeviceParams($hash); + my $numparams= 0; + $numparams= $#prms+1 if(defined($prms[0])); + #Log 5, "ECMDDevice $classname requires $numparams parameter(s): ". join(" ", @prms); + + # keep only the parameters + shift @a; shift @a; shift @a; + + # verify identical number of parameters + if($numparams != $#a+1) { + my $err= "$name error: wrong number of parameters"; + Log 1, $err; + return $err; + } + + # set parameters + for(my $i= 0; $i< $numparams; $i++) { + $hash->{fhem}{params}{$prms[$i]}= $a[$i]; + } + return undef; +} + +1; diff --git a/fhem/FHEM/91_notify.pm b/fhem/FHEM/91_notify.pm index e70d36fa9..4753d2e6f 100755 --- a/fhem/FHEM/91_notify.pm +++ b/fhem/FHEM/91_notify.pm @@ -23,7 +23,7 @@ notify_Define($$) { my ($hash, $def) = @_; my ($name, $type, $re, $command) = split("[ \t]+", $def, 4); - + if(!$command) { if($hash->{OLDDEF}) { # Called from modify, where command is optional (undef, $command) = split("[ \t]+", $hash->{OLDDEF}, 2); @@ -62,21 +62,13 @@ notify_Exec($$) $s = "" if(!defined($s)); if($n =~ m/^$re$/ || "$n:$s" =~ m/^$re$/) { my (undef, $exec) = split("[ \t]+", $ntfy->{DEF}, 2); - $exec = SemicolonEscape($exec); - $exec =~ s/%%/____/g; - my $extsyntax= 0; - $extsyntax+= ($exec =~ s/%TYPE/$t/g); - $extsyntax+= ($exec =~ s/%NAME/$n/g); - $extsyntax+= ($exec =~ s/%EVENT/$s/g); - if(!$extsyntax) { - $exec =~ s/%/$s/g; - } - $exec =~ s/____/%/g; - - $exec =~ s/@@/____/g; - $exec =~ s/@/$n/g; - $exec =~ s/____/@/g; + my %specials= ( + "%NAME" => $n, + "%TYPE" => $t, + "%EVENT" => $s + ); + $exec= EvalSpecials($exec, %specials); my $r = AnalyzeCommandChain(undef, $exec); Log 3, $r if($r); diff --git a/fhem/docs/commandref.html b/fhem/docs/commandref.html index 2988f1a58..6eeb03114 100644 --- a/fhem/docs/commandref.html +++ b/fhem/docs/commandref.html @@ -80,6 +80,8 @@ CUL_HOERMANN   CUL_RFR   CUL_WS   + ECMD   + ECMDDevice   DS18S20   EM   EMEM   @@ -1262,7 +1264,7 @@ A line ending with \ will be concatenated with the next one, so long lines
  • eventMap
    Exchange event or command names to a device specific version. This - will also be used to exchange set arguments (if possible). + will also be used to exchange set arguments (if possible). Example:
      attr store eventMap on:open off:closed
      set store open @@ -2250,7 +2252,7 @@ A line ending with \ will be concatenated with the next one, so long lines send a list of "raw" commands. The first command will be immediately sent, the next one after the previous one is acked by the target. The length will be computed automatically, and the message counter will be - incremented if the first tw charcters are ++. + incremented if the first tw charcters are ++. Example (enable AES):
                set hm1 raw ++A001F100001234560105000000001\
                            ++A001F10000123456010802010AF10B000C00\
      @@ -2308,8 +2310,8 @@ A line ending with \ will be concatenated with the next one, so long lines
           
    • ignore
    • showtime
    • loglevel
    • -
    • hmClass, - model, +
    • hmClass, + model, subType
      These attributes are set automatically after a successful pairing. They are not supposed to be set by hand, and are necessary in order to @@ -3589,6 +3591,262 @@ Attributes:
    + + +

    ECMD

    +
      +
      + Any physical device with request/response-like communication capabilities + over a TCP connection can be defined as ECMD device. A practical example + of such a device is the AVR microcontroller board AVR-NET-IO from + Pollin with + ECMD-enabled + Ethersex firmware.

      + + A physical ECMD device can host any number of logical ECMD devices. Logical + devices are defined as ECMDDevices in fhem. + ADC 0 to 3 and I/O port 0 to 3 of the above mentioned board + are examples of such logical devices. ADC 0 to 3 all belong to the same + device class ADC (analog/digital converter). I/O port 0 to 3 belong to the device + class I/O port. By means of extension boards you can make your physical + device drive as many logical devices as you can imagine, e.g. IR receivers, + LC displays, RF receivers/transmitters, 1-wire devices, etc.

      + + Defining one fhem module for any device class would create an unmanageable + number of modules. Thus, an abstraction layer is used. You create a device class + on the fly and assign it to a logical ECMD device. The + class definition + names the parameters of the logical device, e.g. a placeholder for the number + of the ADC or port, as well as the get and set capabilities. Worked examples + are to be found in the documentation of the ECMDDevice device. +

      + + + Define +

        + define <name> ECMD telnet <IPAddress:Port> +

        + + Defines a physical ECMD device. The keyword telnet is fixed.

        + + Example: +
          + define AVRNETIO ECMD telnet 192.168.0.91:2701
          +
        +
        +
      + + + Set +
        + set <name> classdef <classname> <filename> +

        + Creates a new device class <classname> for logical devices. + The class definition is in the file <filename>. You must + create the device class before you create a logical device that adheres to + that definition. +

        + Example: +
          + define AVRNETIO classdef /etc/fhem/ADC.classdef
          +
        +
        +
      + + + + Get +
        + get <name> raw <command> +

        + Sends the command <command> to the physical ECMD device + <name> and reads the response. +
      +

      + + + Class definition +

      +
        + + The class definition for a logical ECMD device class is contained in a text file. + The text file is made up of single lines. Empty lines and text beginning with # + (hash) are ignored. Therefore make sure not to use hashes in commands.
        + + The following commands are recognized in the device class definition:

        +
          +
        • params <parameter1> [<parameter2> [<parameter3> ... ]]

          + Declares the names of the named parameters that must be present in the + definition of the logical ECMD device. +

          +
        • + +
        • set <commandname> cmd { <perl special> } +

          + Declares a new set command <commandname>. +

          +
        • + +
        • get <commandname> cmd { <perl special> } +

          + Declares a new get command <commandname>. +

          +
        • + +
        • + set <name> params <parameter1> [<parameter2> [<parameter3> ... ]] + get <name> params <parameter1> [<parameter2> [<parameter3> ... ]] +

          + Declares the names of the named parameters that must be present in the + set or get command <name>. Be careful not to use a parameter name that + is already used in the device definition (see params above). +

          +
        • + +
        + + The perl specials in the definitions of the set and get commands can contain macros. Apart from the rules + outlined in the documentation of perl specials in fhem, the following rules apply:

        +
          +
        • The character @ will be replaced with the device + name. To use @ in the text itself, use the double mode (@@).
        • + +
        • The macro %NAME will expand to the device name (same as @).
        • + +
        • The macro %<parameter> will expand to the current value of the + named parameter. This can be either a parameter from the device definition or a parameter + from the set or get command.
        • + +
        • The macro substitution occurs before perl evaluates the expression. It is a plain text substitution.
        • + +
        • If in doubt what happens, run the commands with loglevel 5 and observe the log file.
        • + +
        +
      + + +

      ECMDDevice

      +
        +
        + + Define +
          + define <name> ECMDDevice <classname> [<parameter1> [<parameter2> [<parameter3> ... ]]] +

          + + Defines a logical ECMD device. The number of given parameters must match those given in + the class definition of the device class <classname>. +

          + + Examples: +
            + define myADC ECMDDevice ADC
            + define myRelais1 ECMDDevice relais 8
            +
          +
          +
        + + + Set +
          + set <name> <commandname> [<parameter1> [<parameter2> [<parameter3> ... ]]] +

          + The number of given parameters must match those given for the set command <commandname> definition in + the class definition.

          + If set <commandname> is invoked the perl special in curly brackets from the command definition + is evaluated and the result is sent to the physical ECMD device. +

          + Example: +
            + get myADC value 3
            +
          +
          +
        + + + + Get +
          + get <name> <commandname> [<parameter1> [<parameter2> [<parameter3> ... ]]] +

          + The number of given parameters must match those given for the get command <commandname> definition in + the class definition.

          + If get <commandname> is invoked the perl special in curly brackets from the command definition + is evaluated and the result is sent to the physical ECMD device. The response from the physical ECMD device is returned + and the state of the logical ECMD device is updated accordingly. +

          + Example: +
            + set myRelais1 on
            +
          +
          +
        + + + Example 1 +

        +
          + The following example shows how to access the ADC of the AVR-NET-IO board from + Pollin with + ECMD-enabled + Ethersex firmware.

          + + The class definition file /etc/fhem/ADC.classdef looks as follows:

          + + get value cmd {"adc get %channel"}
          + get value params channel
          +
          +
          + In the fhem configuration file or on the fhem command line we do the following:

          + + define AVRNETIO telnet 192.168.0.91:2701 # define the physical device
          + set AVRNETIO classdef ADC /etc/fhem/ADC.classdef # define the device class ADC
          + define myADC ADC # define the logical device myADC with device cass ADC
          + get myADC value 1 # retrieve the value of analog/digital converter number 1
          +
          +
          + The get command is evaluated as follows: get value has one named parameter + channel. In the example the literal 1 is given and thus %channel + is replaced by 1 to yield "adc get 1" after macro substitution. Perl + evaluates this to a literal string which is send as a plain ethersex command to the AVR-NET-IO. The + board returns something like 024 for the current value of analog/digital converter number 1. +

          + +
        +
      + + Example 2 +

      +
        + The following example shows how to switch a relais driven by pin 3 (bit mask 0x08) of I/O port 2 on for + one second and then off again.

        + + The class definition file /etc/fhem/relais.classdef looks as follows:

        + + params pinmask
        + set on cmd {"io set ddr 2 ff\nioset port 2 0%pinmask\nwait 1000\nio set port 2 00"}
        +
        +
        + In the fhem configuration file or on the fhem command line we do the following:

        + + define AVRNETIO telnet 192.168.0.91:2701 # define the physical device
        + set AVRNETIO classdef relais /etc/fhem/relais.classdef # define the device class relais
        + define myRelais 8 # define the logical device myRelais with pin mask 8
        + set myRelais on # execute the "on" command
        +
        +
        + The set command is evaluated as follows: %pinmask + is replaced by 8 to yield + "io set ddr 2 ff\nioset port 2 08\nwait 1000\nio set port 2 00" after macro substitution. Perl + evaluates this to a literal string which is send as a plain ethersex command to the AVR-NET-IO line by line. +

        + +
      +
    + + + +

    M232

      @@ -3954,8 +4212,8 @@ audio
        define <name> OREGON <deviceid>

        - <deviceid> is the device identifier of the Oregon sensor. It consists of the sensors name and a one byte hex string (00-ff) that identifies the sensor. The define statement with the deviceid is generated automatically by autocreate. The following sensor names are used: -BTHR918, BTHR918N, PCR800 RGR918, RTGR328N, THN132N, THGR228N, THGR328N, THGR918, THR128, THWR288A, THGR810, UV138, UVN800, WGR918, WGR800, WTGR800_A, WTGR800_T. + <deviceid> is the device identifier of the Oregon sensor. It consists of the sensors name and a one byte hex string (00-ff) that identifies the sensor. The define statement with the deviceid is generated automatically by autocreate. The following sensor names are used: +BTHR918, BTHR918N, PCR800 RGR918, RTGR328N, THN132N, THGR228N, THGR328N, THGR918, THR128, THWR288A, THGR810, UV138, UVN800, WGR918, WGR800, WTGR800_A, WTGR800_T.
        The one byte hex string is generated by the Oregon sensor when is it powered on. The value seems to be randomly generated. This has the advantage that you may use more than one Oregon sensor of the same type even if it has no switch to set a sensor id. For exampple the author uses three BTHR918 sensors at the same time. All have different deviceids. The drawback is that the deviceid changes after changing batteries.

        @@ -4243,7 +4501,7 @@ The one byte hex string is generated by the Oregon sensor when is it powered on. Oregon Scientific weather sensors and RFXCOM RFXMeter devices. If you need to process other devices that are supported by RFXCOM, you have to implement a new parsing module.
        See http://www.rfxcom.com/oregon.htm of - Oregon Scientific weather sensors that could be received by the RFXCOM receivers. See http://www.rfxcom.com/sensors.htm for RFXMeter device. + Oregon Scientific weather sensors that could be received by the RFXCOM receivers. See http://www.rfxcom.com/sensors.htm for RFXMeter device. Please note that not all Oregon sensors are currently implemented in the parser module right now. The parsing modules are based on the Perl xPL project parsing modules. Thanks to Mark Hindess from the xPL project for writing this code.
        diff --git a/fhem/fhem.pl b/fhem/fhem.pl index 38a0ce0dc..7d289afac 100755 --- a/fhem/fhem.pl +++ b/fhem/fhem.pl @@ -164,7 +164,7 @@ 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: fhem.pl,v 1.121 2011-01-02 14:45:53 rudolfkoenig Exp $'; +my $cvsid = '$Id: fhem.pl,v 1.122 2011-01-22 21:53:18 neubert Exp $'; my $namedef = "where is either:\n" . "- a single device name\n" . @@ -318,6 +318,8 @@ Log 0, "Server started (version $attr{global}{version}, pid $$)"; ################################################ # Main Loop sub MAIN {MAIN:}; #Dummy + +my $errcount= 0; while (1) { my ($rout, $rin) = ('', ''); @@ -329,6 +331,9 @@ while (1) { vec($rin, fileno($client{$c}{fd}), 1) = 1; } + # for documentation see + # man 2 select + # http://perldoc.perl.org/functions/select.html my $timeout = HandleTimeout(); $timeout = $readytimeout if(keys(%readyfnlist) && (!defined($timeout) || $timeout > $readytimeout)); @@ -340,21 +345,27 @@ while (1) { 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) { # BADF, don't want to "use errno.ph" + 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 0, "ERROR: Found & deleted bad fileno for $p"; + 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 / $!\n"); + die("Select error $nfound ($err)\n"); + } else { + $errcount= 0; } ############################### @@ -371,7 +382,7 @@ while (1) { next if(!$readyfnlist{$p}); # due to rereadcfg / delete if(CallFn($readyfnlist{$p}{NAME}, "ReadyFn", $readyfnlist{$p})) { - if($readyfnlist{$p}) { # delete itself inside ReadyFn + if($readyfnlist{$p}) { # delete itself inside ReadyFn CallFn($readyfnlist{$p}{NAME}, "ReadFn", $readyfnlist{$p}); } @@ -618,18 +629,9 @@ AnalyzeCommandChain($$) ##################################### sub -AnalyzeCommand($$) +AnalyzePerlCommand($$) { - my ($cl, $cmd) = @_; - - $cmd =~ s/^(\\\n|[ \t])*//; # Strip space or \\n at the begginning - $cmd =~ s/[ \t]*$//; - - - Log 5, "Cmd: >$cmd<"; - return if(!$cmd); - - if($cmd =~ m/^{.*}$/s) { # Perl code + my ($cl, $cmd) = @_; $cmd =~ s/\\ *\n/ /g; # Multi-line # Make life easier for oneliners: @@ -650,7 +652,22 @@ AnalyzeCommand($$) $ret = $@ if($@); syswrite($client{$cl}{fd}, "$ret\n") if($ret && $cl); 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 if(!$cmd); + + if($cmd =~ m/^{.*}$/s) { # Perl code + return AnalyzePerlCommand($cl, $cmd); } if($cmd =~ m/^"(.*)"$/s) { # Shell code, always in bg @@ -727,7 +744,7 @@ devspec2array($) push @ret, $l if($attr{$l}{$lattr} && (!$re || $attr{$l}{$lattr} =~ m/$re/)); } - } + } $isattr = 1; next; } @@ -962,7 +979,7 @@ CommandSave($$) } } foreach my $a (sort keys %{$attr{$d}}) { - next if($d eq "global" && + next if($d eq "global" && ($a eq "configfile" || $a eq "version")); print SFH "attr $d $a $attr{$d}{$a}\n"; } @@ -1337,13 +1354,13 @@ CommandList($$) if($defs{$sdev} && $defs{$sdev}{$arg[1]}) { - $str .= $sdev . " " . + $str .= $sdev . " " . $defs{$sdev}{$arg[1]} . "\n"; } elsif($defs{$sdev} && $defs{$sdev}{READINGS} && $defs{$sdev}{READINGS}{$arg[1]}) { - $str .= $sdev . " ". + $str .= $sdev . " ". $defs{$sdev}{READINGS}{$arg[1]}{TIME} . " " . $defs{$sdev}{READINGS}{$arg[1]}{VAL} . "\n"; } @@ -1567,7 +1584,7 @@ CommandAttr($$) my ($cl, $param) = @_; my $ret = undef; my @a; - + @a = split(" ", $param, 3) if($param); return "Usage: attr []\n$namedef" @@ -1777,7 +1794,7 @@ HandleTimeout() $nextat = 0; ############# # Check the internal list. - foreach my $i (sort { $intAt{$a}{TRIGGERTIME} <=> + foreach my $i (sort { $intAt{$a}{TRIGGERTIME} <=> $intAt{$b}{TRIGGERTIME} } keys %intAt) { my $tim = $intAt{$i}{TRIGGERTIME}; my $fn = $intAt{$i}{FN}; @@ -1925,6 +1942,39 @@ SemicolonEscape($) 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; + # 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