############################################## # $Id: 98_GAEBUS.pm 1 2015-07-03 00:00:00 Andreas Goebel $ # derived from 00_TUL.pm # # 17.07.2015 : A.Goebel : initiale Version mit loop, readingname via attribut, keine writes # 21.07.2015 : A.Goebel : start implementation for "set .. write" # 23.07.2015 : A.Goebel : event-on-change-reading added to attributes # 08.09.2015 : A.Goebel : limit number of socket-open retries in GetUpdates loop package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use IO::Socket; use IO::Select; sub GAEBUS_Attr(@); sub GAEBUS_OpenDev($$); sub GAEBUS_CloseDev($); sub GAEBUS_SimpleWrite(@); sub GAEBUS_Disconnected($); sub GAEBUS_Shutdown($); sub GAEBUS_doEbusCmd($$$$); sub GAEBUS_GetUpdates($); my %gets = ( # Name, Data to send to the GAEBUS, Regexp for the answer # "raw" => ["r", '.*'], ); my %getsToRead = (); my %sets = ( # "raw" => "", "reopen" => [] ); my $allSetParams = ""; my $allGetParams = ""; my $delimiter = "~"; my $attrsDefault = "do_not_notify:1,0 dummy:1,0 showtime:1,0 loglevel:0,1,2,3,4,5,6 event-on-change-reading"; my %ebusCmd = (); sub GAEBUS_Initialize($) { my ($hash) = @_; # Normal devices $hash->{DefFn} = "GAEBUS_Define"; $hash->{UndefFn} = "GAEBUS_Undef"; $hash->{GetFn} = "GAEBUS_Get"; $hash->{SetFn} = "GAEBUS_Set"; #$hash->{StateFn} = "GAEBUS_SetState"; $hash->{AttrFn} = "GAEBUS_Attr"; $hash->{AttrList} = $attrsDefault; $hash->{ShutdownFn} = "GAEBUS_Shutdown"; GAEBUS_ReadCSV($hash); $allSetParams = ""; foreach my $setval (sort keys %sets) { Log3 ($hash, 4, "GAEBUS Initialize params for set: $setval"); if ( (@{$sets{$setval}}) > 0) { $allSetParams .= $setval.":".join (",", @{$sets{$setval}})." "; } else { $allSetParams .= $setval." "; } #Log3 ($hash, 2, "GAEBUS Initialize: $setval:$allSetParams"); } $allGetParams = ""; foreach my $getval (sort keys %gets) { Log3 ($hash, 4, "GAEBUS Initialize params for get: $getval"); if ( (@{$gets{$getval}}) > 0) { $allGetParams .= $getval.":".join (",", @{$gets{$getval}})." "; } else { $allGetParams .= $getval." "; } #Log3 ($hash, 2, "GAEBUS Initialize: $getval:$allGetParams"); } } ##################################### sub GAEBUS_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); if(@a < 3) { my $msg = "wrong syntax: define GAEBUS [:] [interval]"; Log (2, $msg); return $msg; } GAEBUS_CloseDev($hash); my $name = $a[0]; my $devaddr = $a[2]; my $interval = $a[3]; $hash->{DeviceName} = $hash->{NAME}; $hash->{DeviceAddress} = $devaddr; $hash->{Interval} = defined ($interval) ? int ($interval) : 150; $hash->{UpdateCnt} = 0; my $ret = GAEBUS_OpenDev($hash, 0); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$hash->{Interval}, "GAEBUS_GetUpdates", $hash, 0); return undef; } ##################################### sub GAEBUS_Undef($$) { my ($hash, $arg) = @_; GAEBUS_CloseDev($hash); return undef; } ##################################### sub GAEBUS_Shutdown($) { my ($hash) = @_; GAEBUS_CloseDev($hash); return undef; } ##################################### sub GAEBUS_Set($@) { my ($hash, @a) = @_; return "\"set GAEBUS\" needs at least one parameter" if(@a < 2); return "Unknown argument $a[1], choose one of " . $allSetParams if(!defined($sets{$a[1]})); my $name = shift @a; my $type = shift @a; my $arg = join("", @a); #return "No $a[1] for dummies" if(IsDummy($name)); #Log3 ($hash, 3, "ebus1: reopen $name"); if ($type eq "reopen") { Log3 ($hash, 3, "ebus1: reopen"); GAEBUS_CloseDev($hash); GAEBUS_OpenDev($hash,0); return undef; } if (defined ($sets{$type})) { unless (grep {$_ eq $arg} @{$sets{$type}}) { return "invalid parameter"; } my $attrname = $type.$delimiter.$arg; $attrname =~ s/\#install/install/; Log3 ($hash, 3, "$name: set $attrname"); addToDevAttrList($name, $attrname); $attr{$name}{$attrname} = "" unless (defined $attr{$name}{$attrname}); } #if($type eq "raw") { # Log3 $hash, 3, "set $name $type $arg"; # GAEBUS_SimpleWrite($hash, $arg); #} return undef; } ##################################### sub GAEBUS_Get($@) { my ($hash, @a) = @_; my $type = $hash->{TYPE}; my $name = $hash->{NAME}; my $arg = (defined($a[2]) ? $a[2] : ""); my $rsp; my $varname = $a[0]; my $readingname = ""; my $readingcmdname = ""; return "\"get $type\" needs at least one parameter" if(@a < 2); # extend possible parameters by the readings defined in attributes my %readings = (); my $actGetParams .= "$allGetParams reading:"; foreach my $oneattr (sort keys %{$attr{$name}}) { my $readingname = $attr{$name}{$oneattr}; my $readingcmdname = $oneattr; $readingname =~ s/:.*//; # only for "r" commands if ($oneattr =~ /^r.*$delimiter.*$delimiter.*$delimiter.*$/) { unless ($readingname =~ /^\s*$/ or $readingname eq "1") { #$actGetParams .= $readingname.","; $readings{$readingname} = $readingcmdname; #Log3 ($name, 2, "$name GetParams $readingname"); } } #$readings{$readingname} = $readingcmdname; #Log3 ($name, 4, "$name Get attr name $readingname"); #Log3 ($name, 4, "$name Get attr cmd $readingcmdname"); } $actGetParams .= join (",", sort keys %readings); # handle "read" parameters and update Reading if ($a[1] eq "reading") { my $readingname = $a[2]; my $readingcmdname = $readings{$readingname}; Log3 ($name, 4, "$name Get name $readingname"); Log3 ($name, 4, "$name Get cmd r $readingcmdname"); my $answer = GAEBUS_doEbusCmd ($hash, "r", $readingname, $readingcmdname); #return (defined($answer ? $answer : "")); return "$answer"; } # other read commands if ($a[1] =~ /^r$delimiter/) { my $readingname = ""; my $readingcmdname = $a[1].$delimiter.$a[2]; Log3 ($name, 3, "$name get cmd v $readingcmdname"); my $answer = GAEBUS_doEbusCmd ($hash, "v", $readingname, $readingcmdname); #return (defined($answer ? $answer : "")); return "$answer"; } # handle commands from %gets and show result from ebusd return "Unknown argument $a[1], choose one of " . $actGetParams if(!defined($gets{$a[1]})); #return "No $a[1] for dummies" if(IsDummy($varname)); return "nix"; #GAEBUS_SimpleWrite($hash, "B".$gets{$a[1]}[0] . $arg); # HERE READ # $rsp = GAEBUS_SimpleRead($hash); if(!defined($rsp)) { GAEBUS_Disconnected($hash); $rsp = "No answer"; } $hash->{READINGS}{$a[1]}{VAL} = $rsp; $hash->{READINGS}{$a[1]}{TIME} = TimeNow(); return "$a[0] $a[1] => $rsp"; } ##################################### sub GAEBUS_SetState($$$$) { my ($hash, $tim, $vt, $val) = @_; return undef; } ######################## sub GAEBUS_CloseDev($) { my ($hash) = @_; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; return if(!$dev); if($hash->{TCPDev}) { $hash->{TCPDev}->close(); delete($hash->{TCPDev}); } #delete($selectlist{"$name.$dev"}); #delete($readyfnlist{"$name.$dev"}); delete($hash->{FD}); $hash->{STATE} = "closed"; } ######################## sub GAEBUS_OpenDev($$) { my ($hash, $reopen) = @_; my $dev = $hash->{DeviceName}; my $name = $hash->{NAME}; my $host = $hash->{DeviceAddress}; my $port = 8888; if($host =~ m/^(.+):(.+)$/) { # host[:port] $host = $1; $port = $2; } $hash->{PARTIAL} = ""; Log3 $hash, 3, "GAEBUS opening $name device $host($port)" if($reopen == 0); # 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}) { Log3 $hash, 5, "GAEBUS NEXT_OPEN prevented opening $name device $host($port)"; return; } my $conn = new IO::Socket::INET ( PeerAddr => "$host", PeerPort => '8888', Proto => 'tcp', Reuse => 0, Timeout => 10 ); if(defined ($conn)) { delete($hash->{NEXT_OPEN}); } else { Log(3, "Can't connect to $dev: $!") if(!$reopen); #$readyfnlist{"$name.$dev"} = $hash; $hash->{STATE} = "disconnected"; $hash->{NEXT_OPEN} = time()+60; return ""; } my $sel = new IO::Select($conn); $hash->{DevType} = 'EBUSD'; $hash->{TCPDev} = $conn; $hash->{FD} = $conn->fileno(); $hash->{SELECTOR} = $sel; #delete($readyfnlist{"$name.$dev"}); #$selectlist{"$name.$dev"} = $hash; if($reopen) { Log3 $hash, 1, "GAEBUS $dev reappeared ($name)"; } else { Log3 $hash, 3, "GAEBUS device opened ($name)"; } #$hash->{STATE}="Initialized"; $hash->{STATE}="Connected"; DoTrigger($name, "CONNECTED") if($reopen); return 0; } sub GAEBUS_Disconnected($) { my $hash = shift; my $dev = $hash->{DeviceName}; my $name = $hash->{NAME}; return if(!defined($hash->{FD})); # Already deleted or RFR Log3 $hash, 1, "$dev disconnected, waiting to reappear"; GAEBUS_CloseDev($hash); #$readyfnlist{"$name.$dev"} = $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 GAEBUS_Attr(@) { my @a = @_; my ($action, $name, $attrname, $attrval) = @a; $attrval = "" unless defined ($attrval); if ($action eq "del") { my $userattr = $attr{$name}{userattr}; #Log3 ($hash, 2, ">$userattr<>$attrname<"); if ( " $userattr " =~ / $attrname / ) { #Log3 ($hash, 2, "match"); # " a" or "^a$" $userattr =~ s/ *$attrname//; if ($userattr eq "") { delete($attr{$name}{userattr}); } else { $attr{$name}{userattr} = $userattr; } } # delete reading if attribute name contains $delimiter if ($attrname =~ /^.*$delimiter/) { my $reading = $attr{$name}{$attrname}; $reading =~ s/:.*//; Log3 ($name, 3, "$name: delete reading: $reading"); delete($defs{$name}{READINGS}{$reading}); } return undef; } elsif ($action eq "set") { if (defined $attr{$name}{$attrname}) { my $oldreading = $attr{$name}{$attrname}; $oldreading =~ s/:.*//; my $newreading = $attrval; $newreading =~ s/:.*//; if ($oldreading ne $newreading) { #Log3 ($name, 2, "$name: adjust reading: $oldreading"); if (defined($defs{$name}{READINGS}{$oldreading})) { unless ($newreading =~ /^1*$/) # matches "1" or "" { #Log3 ($name, 2, "$name: change attribute $attrname ($oldreading -> $newreading)"); $defs{$name}{READINGS}{$newreading}{VAL} = $defs{$name}{READINGS}{$oldreading}{VAL}; $defs{$name}{READINGS}{$newreading}{TIME} = $defs{$name}{READINGS}{$oldreading}{TIME}; } delete($defs{$name}{READINGS}{$oldreading}); } } } } Log3 (undef, 2, "called GAEBUS_Attr($a[0],$a[1],$a[2],<$a[3]>)"); return undef; } sub GAEBUS_ProcessCSV($$) { my ($hash, $file) = @_; if (!open (CSV, "<$file")) { Log3 ($hash, 2, "GAEBUS: cannot open $file"); return undef; } else { my $line; my $buffer = ""; my $actCircuit = ""; my %circuits = (); my $defCircuit = "$file"; $defCircuit =~ s,^.*/,,; $defCircuit =~ s/\.csv$//; while () { next if /^#/; next if /^\s$/; s/\r//; chomp; $line = $_; $line =~ s/ /_/g; # no blanks in names and comments $line =~ s/$delimiter/_/g; # clean up the delimiter within the text #$line =~ s/\|/_/g; # later used as delimiter in arguments my ($io, $circuit, $vname, $comment, @params) = split (",", $line, 5); # handle defaults: "^*" if ($io =~ /^\*(.*)/) { my $rwkey = $1; if ($circuit ne "") { #print "new circuit found $rwkey: $circuit\n"; $circuits{$rwkey} = $circuit; next; } } # collect variables foreach my $dir (split (";", $io.";")) { if ($circuit ne "") { $actCircuit = $circuit; } elsif (defined ($circuits{$dir})) { $actCircuit = $circuits{$dir}; } else { $actCircuit = $defCircuit; } #printf "%-3s %-12s %-40s %-s\n", $dir, $actCircuit, $vname, $comment; #Log3 $hash, 3, printf "%-3s %-12s %-40s %-s\n", $dir, $actCircuit, $vname, $comment; #Log3 $hash, 3, "$dir, $actCircuit, $vname, $comment"; my $dirSimple = substr($dir, 0,1); if ($dirSimple =~ /^[rw]/) { my $rkey = join (";", ($dirSimple, $actCircuit, $vname)); $ebusCmd{$rkey} = $comment; } } #print "$io $actCircuit\n"; } close (CSV); } } sub GAEBUS_ReadCSV($) { my $hash = shift; my $dir = "./ebusd"; %ebusCmd = (); if (opendir INDIR, $dir) { my @infiles = grep /^[^\.].*\.csv$/, readdir INDIR; # all files exept those starting with "." foreach my $file (@infiles) { Log3 ($hash, 4, "GAEBUS: process config $file"); GAEBUS_ProcessCSV($hash, $dir."/".$file); } closedir INDIR; %sets = ( "reopen" => [] ); %gets = ( ); my $comment; foreach my $key (sort keys %ebusCmd) { $comment = $ebusCmd{$key}; my ($io,$class,$var) = split (";", $key, 3); push @{$sets{$io.$delimiter.$class}}, $var.$delimiter.$comment if ($io eq "r" or $io eq "w"); push @{$gets{$io.$delimiter.$class}}, $var.$delimiter.$comment if ($io eq "r" or $io eq "u"); Log3 ($hash, 5, "GAEBUS: add attr $key $comment"); } } else { Log3 ($hash, 2, "cannot open dir $dir"); } } sub GAEBUS_doEbusCmd($$$$) { my $hash = shift; my $action = shift; # "r" = set reading, "v" = verbose mode my $readingname = shift; my $readingcmdname = shift; my $actMessage; my $name = $hash->{NAME}; if (($hash->{STATE} ne "Connected") or (!$hash->{TCPDev}->connected()) ) { Log3 ($name, 2, "$name device closed. Try to reopen"); GAEBUS_CloseDev($hash); GAEBUS_OpenDev($hash,1); return undef unless ($hash->{STATE} eq "Connected"); } my $timeout = 1.8; $timeout = 10 if ($action eq "v"); my ($io,$class,$var,$comment) = split ($delimiter, $readingcmdname, 4); my $cmd = "$io -f "; $cmd .= "-v " if ($action eq "v"); $cmd .= "-c $class $var"; Log3 ($name, 3, "$name execute $cmd"); if ($hash->{SELECTOR}->can_read(0)) { sysread ($hash->{TCPDev}, $actMessage, 4096); $actMessage =~ s/\n//g; Log3 ($name, 2, "$name old answer $actMessage\n"); } syswrite ($hash->{TCPDev}, $cmd."\n"); if ($hash->{SELECTOR}->can_read($timeout)) { #Log3 ($name, 2, "$name try to read"); sysread ($hash->{TCPDev}, $actMessage, 4096); $actMessage =~ s/\n//g; $actMessage =~ s/;/ /g; Log3 ($name, 3, "$name answer $action $readingname $actMessage"); unless ($actMessage =~ "Usage:") { unless ($actMessage =~ /^ERR:/) { if ($action eq "r") { if (defined ($readingname)) { readingsSingleUpdate ($hash, $readingname, "$actMessage", 1); } } } } ($action eq "r") ? return undef : return $actMessage; } else { return "timeout reading answer for ($readingname) $cmd"; } } sub GAEBUS_GetUpdates($) { my ($hash) = @_; my $name = $hash->{NAME}; my $readingname = ""; my $tryOpenCnt = 2; # no of tries to open the device, before giving up $hash->{UpdateCnt} = $hash->{UpdateCnt} + 1; foreach my $oneattr (keys %{$attr{$name}}) { # only for "r" commands if ($oneattr =~ /^r.*$delimiter.*$delimiter.*$delimiter.*$/) { my ($readingname, $doCntNo) = split (":", $attr{$name}{$oneattr}, 2); # split name from cycle number $doCntNo = 1 unless (defined ($doCntNo)); Log3 ($name, 5, "$name GetUpdates: $readingname:$doCntNo"); next if ($readingname =~ /^\s*$/); next if ($readingname eq "1"); #Log3 ($name, 2, "$name check modulo ".$hash->{UpdateCnt}." mod $doCntNo -> ".($hash->{UpdateCnt} % $doCntNo)); if (($hash->{UpdateCnt} % $doCntNo) == 0) { my $answer = GAEBUS_doEbusCmd ($hash, "r", $readingname, $oneattr); } # limit number of reopens if ebusd cannot be reached if (($hash->{STATE} ne "Connected") or (!$hash->{TCPDev}->connected()) ) { if (--$tryOpenCnt <= 0) { Log3 ($name, 2, "$name: not connected, stop GetUpdates loop"); last; } } } } InternalTimer(gettimeofday()+$hash->{Interval}, "GAEBUS_GetUpdates", $hash, 1); } 1; =pod =begin html

GAEBUS

    The GAEBUS module is the representation of a Ebus connector in FHEM. The GAEBUS module is designed to connect to ebusd (ebus daemon) via a socket connection (default is port 8888)
    Define
      define <name> GAEBUS <device-addr>[:<port>] [<interval>];

      <device-addr>[:<port>] specifies the host:port of the ebusd device. E.g. 192.168.0.244:8888 or servername:8888. When using the standard port, the port can be omitted.

      Example:
      define ebus1 GAEBUS localhost 300

      When initializing the object the configuration of the ebusd (.csv files from /etc/ebusd) are read.
      The files need to be copied into a directory "ebusd" (normally /opt/fhem/ebusd) on the server running fhem.

    Set
    • reopen
      Will close and open the socket connection.

    • [r]~<class> <variable-name>~<comment from csv>
      Will define a attribute with the following syntax:
      [r]~<class>~<variable-name>~<comment from csv>
      Valid combinations are read from the .csv files in directory "ebusd" and are selectable
      Values from the attributes will be used as the name for the reading which are read from ebusd in the interval specified.
    Get
    • reading <reading-name>
      Will read the actual value form ebusd and update the reading.

    • [r]~<class> <variable-name>~<comment from csv>
      Will read this variable from the ebusd and show the result as a popup.
      Valid combinations are read from the .csv files in directory "ebusd" and are selectable

    Attributes
    • do_not_notify

    • dummy

    • showtime

    • loglevel

    • Attributes of the format
      [r]~<class>~<variable-name>~<comment from csv>
      define variables that can be retrieved from the ebusd. They will appear when they are defined by a "set" command as described above.
      The value assigned to an attribute specifies the name of the reading for this variable.
      The name of the reading can be suffixed by "<number>" which is a multiplicator for the evaluation within the specified interal. (eg. OutsideTemp:3 will evaluate this reading every 3-th cycle)

=end html =cut