From ab7de5ba10465f213ac9ca34b5bd3e20ca77763c Mon Sep 17 00:00:00 2001 From: jamesgo <> Date: Sat, 24 Sep 2016 06:49:33 +0000 Subject: [PATCH] 98_GAEBUS: move from contrib into FHEM tree git-svn-id: https://svn.fhem.de/fhem/trunk@12204 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/98_GAEBUS.pm | 1243 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1243 insertions(+) create mode 100644 fhem/FHEM/98_GAEBUS.pm diff --git a/fhem/FHEM/98_GAEBUS.pm b/fhem/FHEM/98_GAEBUS.pm new file mode 100644 index 000000000..4e56c0262 --- /dev/null +++ b/fhem/FHEM/98_GAEBUS.pm @@ -0,0 +1,1243 @@ +############################################# +# $Id$ +# 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 +# 10.09.2015 : A.Goebel : fix html code of commandref +# 11.09.2015 : A.Goebel : add attribute "ebusWritesEnable:0,1" +# 11.09.2015 : A.Goebel : add set w~ commands to set attributes for writing +# 11.09.2015 : A.Goebel : add set command to write to ebusd +# 13.09.2015 : A.Goebel : increase timeout for reads from ebusd from 1.8 to 5.0 +# 14.09.2015 : A.Goebel : use utf-8 coding to display values from ".csv" files +# 14.09.2015 : A.Goebel : add optional parameter [FIELD[.N]] of read from ebusd to reading name +# 15.09.2015 : A.Goebel : get rid of perl warnings when attribute value is empty +# 16.09.2015 : A.Goebel : allow write to parameters protected by #install +# 21.09.2015 : A.Goebel : implement BlockingCall Interface +# 07.10.2015 : A.Goebel : beautify and complete commandref +# 12.10.2015 : A.Goebel : fix handling of timeouts in BlockingCall Interface (additional parameter in doEbusCmd forces restart (no shutdown restart)) +# timeout for reads increased +# 19.10.2015 : A.Goebel : add attribute disable to disable loop to collect readings +# 05.11.2015 : A.Goebel : add support for "h" (broadcast update) commands from csv, handle them equal to (r)ead +# 09.11.2015 : A.Goebel : add support for multiple readings generated from one command to ebusd +# ebusd may return a list of values like "52.0;43.0;8.000;41.0;45.0;error" +# defining a reading "VL;RL;dummy;VLWW;RLWW" will create redings VL, RL, VLWW and RLWW +# 04.12.2015 : A.Goebel : add event-min-interval to attributes +# 14.12.2015 : A.Goebel : add read possible commmands for ebusd using "find -f" instead of reading the ".csv" files directly ("get ebusd_find") +# 25.01.2016 : A.Goebel : fix duplicate log entries for readings +# 05.02.2016 : A.Goebel : add valueFormat attribute +# 18.08.2016 : A.Goebel : fix workarround for perl warning with keys of hash reference +# 30.08.2016 : A.Goebel : add reading "state_ebus" containing output from "state" of ebusd +# 16.09.2016 : A.Goebel : add reset "state_ebus" if ebus is not connected + +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use IO::Socket; +use IO::Select; +use Encode; +use Blocking; + +sub GAEBUS_Attr(@); + +sub GAEBUS_OpenDev($$); +sub GAEBUS_CloseDev($); +sub GAEBUS_Disconnected($); +sub GAEBUS_Shutdown($); + +sub GAEBUS_doEbusCmd($$$$$$$); +sub GAEBUS_GetUpdates($); + +sub GAEBUS_GetUpdatesDoit($); +sub GAEBUS_GetUpdatesDone($); +sub GAEBUS_GetUpdatesAborted($); + +sub GAEBUS_State($); + +my %gets = ( # Name, Data to send to the GAEBUS, Regexp for the answer +); + +my %sets = ( + #"reopen" => [] +); + +my %setsForWriting = (); + +my $allSetParams = ""; +my $allSetParamsForWriting = ""; +my $allGetParams = ""; +my $delimiter = "~"; + +my $attrsDefault = "do_not_notify:1,0 disable:1,0 dummy:1,0 showtime:1,0 loglevel:0,1,2,3,4,5,6 event-on-change-reading event-min-interval ebusWritesEnabled:0,1 valueFormat:textField-long"; +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"; + + + %sets = ( "reopen" => [] ); + %gets = ( "ebusd_find" => [], "ebusd_info" => [] ); + %setsForWriting = ( ); + + GAEBUS_initParams($hash); + +} + +##################################### +sub +GAEBUS_initParams ($) +{ + my ($hash) = @_; + + # bulid set and get Params and store them in + # $allSetParams + # $allSetParamsForWriting + # $allGetParams + + $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"); + } + + $allSetParamsForWriting = ""; + foreach my $setval (sort keys %setsForWriting) + { + Log3 ($hash, 4, "GAEBUS Initialize params for setsForWriting: $setval"); + if ( (@{$setsForWriting{$setval}}) > 0) + { + $allSetParamsForWriting .= $setval.":".join (",", @{$setsForWriting{$setval}})." "; + } + else + { + $allSetParamsForWriting .= $setval." "; + } + #Log3 ($hash, 2, "GAEBUS Initialize: $setval:$allSetParamsForWriting"); + } + + $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()+10, "GAEBUS_GetUpdates", $hash, 0); + + return undef; +} + + +##################################### +sub +GAEBUS_Undef($$) +{ + my ($hash, $arg) = @_; + GAEBUS_CloseDev($hash); + + BlockingKill($hash->{helper}{RUNNING_PID}) if(defined($hash->{helper}{RUNNING_PID})); + + 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); + + 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"); + + #Log3 ($hash, 2, "$name: set $arg $type invalid parameter"); + + if ($type eq "reopen") { + Log3 ($hash, 3, "ebus1: reopen"); + GAEBUS_CloseDev($hash); + GAEBUS_OpenDev($hash,0); + return undef; + } + + # handle commands defined in %sets + + 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}); + + return undef; + } + + # + # extend possible parameters by the readings defined for writing in attributes + # + + + my %writings = (); + + my $actSetParams = "$allSetParams "; + + my $ebusWritesEnabled = (defined($attr{$name}{"ebusWritesEnabled"})) ? $attr{$name}{"ebusWritesEnabled"} : 0; + + if ($ebusWritesEnabled) { + $actSetParams .= "$allSetParams$allSetParamsForWriting " if ($ebusWritesEnabled); + + foreach my $oneattr (sort keys %{$attr{$name}}) + { + my $readingname = $attr{$name}{$oneattr}; + my $readingcmdname = $oneattr; + $readingname =~ s/ .*//; + $readingname =~ s/:.*//; + + # only for "w" commands + if ($oneattr =~ /^w.*$delimiter.*$delimiter.*$delimiter.*$/) + { + unless ($readingname =~ /^\s*$/ or $readingname eq "1") + { + $writings{$readingname} = $readingcmdname; + #Log3 ($name, 2, "$name SetParams $readingname"); + } + } + + #Log3 ($name, 4, "$name Set attr name $readingname"); + #Log3 ($name, 4, "$name Set attr cmd $readingcmdname"); + } + $actSetParams .= join (" ", sort keys %writings); + + + # handle write commands (which were defined above) + + if (defined ($setsForWriting{$type})) + { + unless (grep {$_ eq $arg} @{$setsForWriting{$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}); + + return undef; + } + + if (defined ($writings{$type})) + { + foreach my $oneattr (sort keys %{$attr{$name}}) + { + next unless ($oneattr =~ /^w.*$delimiter.*$delimiter.*$delimiter.*$/); + + my $readingname = $attr{$name}{$oneattr}; + next if ($readingname ne $type); + + my $answer = GAEBUS_doEbusCmd ($hash, "w", $readingname, $oneattr, $arg, "", 0); + return "$answer"; + } + } + + } + + + return "Unknown argument $type, choose one of " . $actSetParams + if(!defined($sets{$type})); + + 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 %readingsCmdaddon = (); + my $actGetParams .= "$allGetParams reading:"; + foreach my $oneattr (sort keys %{$attr{$name}}) + { + my ($readingnameX, $cmdaddon) = split (" ", $attr{$name}{$oneattr}, 2); + $cmdaddon = "" unless (defined ($cmdaddon)); + + next unless defined ($readingnameX); + next if ($readingnameX =~ /^\s*$/); + next if ($readingnameX eq "1"); + + my ($readingname, $doCntNo) = split (":", $readingnameX, 2); # split name from cycle number + $doCntNo = 1 unless (defined ($doCntNo)); + + #my $readingname = $attr{$name}{$oneattr}; + my $readingcmdname = $oneattr; + $readingname =~ s/ .*//; + $readingname =~ s/:.*//; + + # only for "r" commands + if ($oneattr =~ /^[rh].*$delimiter.*$delimiter.*$delimiter.*$/) + { + $readings{$readingname} = $readingcmdname; + $readingsCmdaddon{$readingname} = $cmdaddon; + + #Log3 ($name, 2, "$name GetParams $readingname"); + } + + #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}; + my $cmdaddon = $readingsCmdaddon{$readingname}; + + Log3 ($name, 4, "$name Get name $readingname"); + Log3 ($name, 4, "$name Get cmd r $readingcmdname"); + + my $answer = GAEBUS_doEbusCmd ($hash, "r", $readingname, $readingcmdname, "", $cmdaddon, 0); + + #return "$answer"; + return undef; + + } + + if ($a[1] eq "ebusd_find") + { + Log3 ($name, 4, "$name Get $a[1]"); + + my $answer = GAEBUS_doEbusCmd ($hash, "f", "", "", "", "", 0); + + return $answer; + } + + if ($a[1] eq "ebusd_info") + { + Log3 ($name, 4, "$name Get $a[1]"); + + my $answer = GAEBUS_doEbusCmd ($hash, "i", "", "", "", "", 0); + + return $answer; + } + + # other read commands + + if ($a[1] =~ /^[rh]$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, "", "", 0); + #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"; +} + +##################################### +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"; + readingsSingleUpdate ($hash, "state_ebus", "unknown", 1); +} + +######################## +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; + + my $hash = $defs{$name}; + + $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/ .*//; + $reading =~ s/:.*//; + + foreach my $r (split /;/, $reading) { + Log3 ($name, 3, "$name: delete reading: $reading"); + delete($defs{$name}{READINGS}{$reading}); + } + } + + if ($attrname eq "valueFormat" and defined ($hash->{helper}{$attrname})) { + delete ($hash->{helper}{$attrname}); + } + + return undef; + } + elsif ($action eq "set") + { + + if ($attrname eq "valueFormat") { + my $attrVal = $attrval; + if( $attrVal =~ m/^{.*}$/s && $attrVal =~ m/=>/ && $attrVal !~ m/\$/ ) { + my $av = eval $attrVal; + if( $@ ) { + Log3 ($hash->{NAME}, 3, $hash->{NAME} ." $attrname: ". $@); + } else { + $attrVal = $av if( ref($av) eq "HASH" ); + } + $hash->{helper}{$attrname} = $attrVal; + + foreach my $key (keys %{ $hash->{helper}{$attrname} }) { + Log3 ($hash->{NAME}, 3, $hash->{NAME} ." $key ".$hash->{helper}{$attrname}{$key}); + } + #return "set HERE??"; + + } else { + # if valueFormat is not verified sucessfully ... the helper is deleted (=not used) + delete $hash->{helper}{$attrname}; + } + return undef; + } + + if (defined $attr{$name}{$attrname}) + { + my $oldreading = $attr{$name}{$attrname}; + $oldreading =~ s/ .*//; + $oldreading =~ s/:.*//; + my $newreading = $attrval; + $newreading =~ s/ .*//; + $newreading =~ s/:.*//; + + my @or = split /;/, $oldreading; + my @nr = split /;/, $newreading; + + for (my $i; $i <= $#or; $i++) + { + if ($or[$i] ne $nr[$i]) + { + #Log3 ($name, 2, "$name: adjust reading: $or[$i]"); + + if (defined($defs{$name}{READINGS}{$or[$i]})) + { + if (defined ($nr[$i] and $nr[$i] ne "dummy" )) + { + unless ($nr[$i] =~ /^1*$/) # matches "1" or "" + { + #Log3 ($name, 2, "$name: change attribute $attrname ($or[$i] -> $nr[$i])"); + + $defs{$name}{READINGS}{$nr[$i]}{VAL} = $defs{$name}{READINGS}{$or[$i]}{VAL}; + $defs{$name}{READINGS}{$nr[$i]}{TIME} = $defs{$name}{READINGS}{$or[$i]}{TIME}; + } + } + + delete($defs{$name}{READINGS}{$or[$i]}); + + } + } + } + } + } + + Log3 (undef, 2, "called GAEBUS_Attr($a[0],$a[1],$a[2],<$a[3]>)"); + + return undef; +} + +sub +GAEBUS_State($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + my $state = ""; + my $actMessage = ""; + + if (($hash->{STATE} eq "Connected") and ($hash->{TCPDev}->connected()) ) + { + + my $timeout = 10; + + syswrite ($hash->{TCPDev}, "state\n"); + if ($hash->{SELECTOR}->can_read($timeout)) + { + sysread ($hash->{TCPDev}, $actMessage, 4096); + $actMessage =~ s/\n$/ /g; + $actMessage =~ s/ {1,}$//; + if ($actMessage =~ /^signal acquired/) { + $state = "ok"; + } + + } + else + { + $state = "no answer"; + Log3 ($name, 2, "$name state $state ($actMessage)"); + } + + } + + return ($state, $actMessage); +} + +sub +GAEBUS_doEbusCmd($$$$$$$) +{ + my $hash = shift; + my $action = shift; # "r" = get reading, "v" = verbose mode, "w" = write to ebus, "f" = execute find to read in config, "i" = execute info + my $readingname = shift; + my $readingcmdname = shift; + my $writeValues = shift; + my $cmdaddon = shift; + my $inBlockingCall = 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,0); + + if ($hash->{STATE} ne "Connected") { + if ($inBlockingCall) { + return ""; + } + else { + return undef; + } + } + } + + #my $timeout = 1.8; + my $timeout = 15.0; + $timeout = 10.0 if ($action eq "v"); + $timeout = 10.0 if ($action eq "w"); + + + my ($io,$class,$var,$comment) = split ($delimiter, $readingcmdname, 4); + + my $cmd = ""; + + if ($action eq "w") { + + $class =~ s/install/#install/; + + $cmd = "$io "; + $cmd .= "-c $class $var "; + $cmd .= "$writeValues"; + + } elsif ($action eq "f") { + $cmd = "find -f -r -w"; + + } elsif ($action eq "i") { + $cmd = "info"; + + } else { + + $cmd = "$io "; + $cmd .= " -f " if ($io ne "h"); + $cmd .= "-v " if ($action eq "v"); + $cmd .= "-c $class $var"; + $cmd .= " $cmdaddon" if ($action eq "r"); + + $cmd =~ s/^h /r /; + } + + Log3 ($name, 3, "$name execute $cmd"); + + if ($hash->{SELECTOR}->can_read(0.1)) + { + sysread ($hash->{TCPDev}, $actMessage, 4096); + $actMessage =~ s/\n//g; + Log3 ($name, 2, "$name old answer $actMessage\n"); + $actMessage = ""; + } + + syswrite ($hash->{TCPDev}, $cmd."\n"); + if (0 and $hash->{SELECTOR}->can_read($timeout)) + { + #Log3 ($name, 2, "$name try to read"); + sysread ($hash->{TCPDev}, $actMessage, 4096); + $actMessage =~ s/\n//g; + #$actMessage =~ s/;/ /g; + } + + if ($hash->{SELECTOR}->can_read($timeout)) { + + my $rbuffer = ""; + + while ($hash->{SELECTOR}->can_read(0.1) and sysread ($hash->{TCPDev}, $rbuffer, 4096)) { + $actMessage .= $rbuffer; + + if ( $rbuffer =~ /\n\n$/ ) + { + Log3 ($name, 3, "$name answer terminated by empty line"); + last; + } + + } + #$actMessage =~ s/\n//g; + + + #Log3 ($name, 3, "$name answer $action $readingname $actMessage"); + + } + else + { + #return "timeout reading answer for ($readingname) $cmd"; + + Log3 ($name, 2, "$name: timeout reading answer for $cmd"); + + return ""; + } + + if ($action eq "f") + { + + %sets = ( "reopen" => [] ); + %gets = ( "ebusd_find" => [], "ebusd_info" => [] ); + %setsForWriting = ( ); + + my $cnt = 0; + foreach my $line (split /\n/, $actMessage) { + $cnt++; + + $line =~ s/ /_/g; # no blanks in names and comments + $line =~ s/$delimiter/_/g; # clean up the delimiter within the text + + Log3 ($name, 3, "$name $line"); + + #my ($io,$class,$var) = split (",", $line, 3); + my ($io, $class, $var, $comment, @params) = split (",", $line, 5); + + $io =~ s/[0-9]//g; + + # drop "memory" + + next if ($class eq "memory"); + next if ($class eq "scan"); + + + push @{$sets{$io.$delimiter.$class}}, $var.$delimiter.$comment if ($io eq "r" or $io eq "h"); + + push @{$setsForWriting{$io.$delimiter.$class}}, $var.$delimiter.$comment if ($io eq "w" or $io eq "wi"); + + push @{$gets{$io.$delimiter.$class}}, $var.$delimiter.$comment if ($io eq "r" or $io eq "h"); + + } + + GAEBUS_initParams($hash); + + Log3 ($name, 3, "$name find done."); + return "$cnt definitions processed"; + + } + if ($action eq "i") + { + #Log3 ($name, 3, "$name info done."); + return "$actMessage"; + + } + + $actMessage =~ s/\n//g; + Log3 ($name, 3, "$name answer $action $readingname $actMessage"); + + my @values = split /;/, $actMessage; + my @targetreading = defined ($readingname) ? split /;/, $readingname : (); + + if ($inBlockingCall) + { + $actMessage = ""; + + for (my $i=0; $i <= $#targetreading; $i++) + { + next if ($targetreading[$i] eq "dummy"); + my $v = defined($values[$i]) ? $values[$i] : ""; + + $actMessage .= $targetreading[$i]."|".$v."|"; + } + + $actMessage =~ s/\|$//; + + # readings will be updated in main fhem process + return $actMessage; + } + + if ($action eq "r") + { + readingsBeginUpdate ($hash); + for (my $i=0; $i <= $#targetreading; $i++) + { + next if ($targetreading[$i] eq "dummy"); + my $v = defined($values[$i]) ? $values[$i] : ""; + + $v = GAEBUS_valueFormat ($hash, $targetreading[$i], $v); + + readingsBulkUpdate ($hash, $targetreading[$i], $v); + } + readingsEndUpdate($hash, 1); + } + + + #if ($inBlockingCall) { + # $actMessage = $readingname."|".$actMessage; + #} + #else { + # + # if ($action eq "r") { + # if (defined ($readingname)) { + # # BlockingCall changes + # readingsSingleUpdate ($hash, $readingname, "$actMessage", 1); + # } + # } + #} + + + return $actMessage; + +} + +sub +GAEBUS_GetUpdates($) +{ + my ($hash) = @_; + + my $name = $hash->{NAME}; + + if (defined($attr{$name}{disable}) and ($attr{$name}{disable} == 1)) { + Log3 $hash, 4, "$name GetUpdates2 is disabled"; + + InternalTimer(gettimeofday()+$hash->{Interval}, "GAEBUS_GetUpdates", $hash, 0); + return; + + } else { + Log3 $hash, 4, "$name start GetUpdates2"; + + } + + $hash->{UpdateCnt} = $hash->{UpdateCnt} + 1; + + $hash->{helper}{RUNNING_PID} = BlockingCall("GAEBUS_GetUpdatesDoit", $name, "GAEBUS_GetUpdatesDone", 120, "GAEBUS_GetUpdatesAborted", $hash) + unless(exists($hash->{helper}{RUNNING_PID})); + +} + +sub +GAEBUS_GetUpdatesDoit($) +{ + my ($string) = (@_); + #my ($name, $nochwas) = split ("|", $string); + my ($name) = $string; + my ($hash) = $defs{$name}; + + my $readingname = ""; + my $tryOpenCnt = 2; # no of tries to open the device, before giving up + + my $readingsToUpdate = ""; + + # don't use socket inherited from fhem by BlockingCall.pm + delete($hash->{TCPDev}); + $hash->{STATE} = "pleaseReconnect"; + my $ret = GAEBUS_OpenDev($hash, 0); + if (($hash->{STATE} ne "Connected") or (!$hash->{TCPDev}->connected()) ) + { + return "$name"; + } + + # syncronize with ebusd + + my ($state, $actMessage) = GAEBUS_State($hash); + if ($state ne "ok") { + Log3 ($name, 2, "$name: ebusd no connection or signal state($state)"); + return "$name"; + } + Log3 ($name, 5, "$name: ebusd state($actMessage)"); + $actMessage =~ s/,.*//; + $readingsToUpdate .= "|state_ebus|".$actMessage; + + foreach my $oneattr (keys %{$attr{$name}}) + { + # only for "r" commands + if ($oneattr =~ /^[rh].*$delimiter.*$delimiter.*$delimiter.*$/) + { + + my ($readingnameX, $cmdaddon) = split (" ", $attr{$name}{$oneattr}, 2); + $cmdaddon = "" unless (defined ($cmdaddon)); + + next unless defined ($readingnameX); + next if ($readingnameX =~ /^\s*$/); + next if ($readingnameX eq "1"); + + my ($readingname, $doCntNo) = split (":", $readingnameX, 2); # split name from cycle number + $doCntNo = 1 unless (defined ($doCntNo)); + + Log3 ($name, 5, "$name GetUpdates: $readingname:$doCntNo"); + + #Log3 ($name, 2, "$name check modulo ".$hash->{UpdateCnt}." mod $doCntNo -> ".($hash->{UpdateCnt} % $doCntNo)); + if (($hash->{UpdateCnt} % $doCntNo) == 0) + { + $readingsToUpdate .= "|".GAEBUS_doEbusCmd ($hash, "r", $readingname, $oneattr, "", $cmdaddon, 1); + } + + # 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; + } + } + + } + } + + # returnvalue for BlockingCall ... done routine + return $name.$readingsToUpdate; + + +} + +sub +GAEBUS_GetUpdatesDone($) +{ + my ($string) = @_; + + return unless(defined($string)); + + my @a = split("\\|",$string); + my ($hash) = $defs{$a[0]}; + my $name = $hash->{NAME}; + + delete($hash->{helper}{RUNNING_PID}); + + #Log3 ($name, 2, "$name: GetUpdatesDoit returned $string"); + + readingsBeginUpdate ($hash); + for (my $i = 1; $i < $#a; $i = $i + 2) + { + my $v = GAEBUS_valueFormat ($hash, $$a[$i], $a[$i+1]); + #readingsBulkUpdate ($hash, $a[$i], $a[$i+1]); + readingsBulkUpdate ($hash, $a[$i], $v); + } + readingsEndUpdate($hash, 1); + + InternalTimer(gettimeofday()+$hash->{Interval}, "GAEBUS_GetUpdates", $hash, 0); + +} + + +sub +GAEBUS_GetUpdatesAborted($) +{ + my ($hash) = @_; + + delete($hash->{helper}{RUNNING_PID}); + + Log3 $hash->{NAME}, 3, "BlockingCall for ".$hash->{NAME}." was aborted"; + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$hash->{Interval}, "GAEBUS_GetUpdates", $hash, 0); +} + +sub +GAEBUS_valueFormat(@) +{ + my ($hash, $reading, $value) = @_; + + return $value unless (defined ($reading)); + + if (ref($hash->{helper}{valueFormat}) eq 'HASH') + { + + if (exists($hash->{helper}{valueFormat}->{$reading})) { + + my $vf = $hash->{helper}{valueFormat}->{$reading}; + return sprintf ("$vf", $value); + } + } + + return $value; + +} + + +1; + +=pod +=item device +=item summary device to communicate with ebusd (a communication bus for heating systems). +=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 no device specific commands are known. Please call "get ebusd_find" to read in supported commands from ebusd.
      + After fresh restart of ebusd it may take a while until all supported devices and their commands are visible.
      + +
    +
    + + + Set +
      +
    • reopen
      + Will close and open the socket connection. +

    • +
    • [r]~<class> <variable-name>~<comment>
      + Will define a attribute with the following syntax:
      + [r]~<class>~<variable-name>~<comment>
      + Valid combinations are read from ebusd (using "get ebusd_find") 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.
      +

    • +
    • [w]~<class> <variable-name>~<comment>
      + Will define a attribute with the following syntax:
      + [w]~<class>~<variable-name>~<comment>
      + They will only appear if the attribute "ebusWritesEnabled" is set to "1"
      + Valid combinations are read from ebusd (using "get ebusd_find") and are selectable.
      + Values from the attributes will be used for set commands to modify parameters for ebus devices
      + Hint: if the values for the attributes are prefixed by "set-" then all possible parameters will be listed in one block
      +

    • +
    + + + Get +
      +
    • ebusd_info
      + Execude info command on ebusd and show result. +

    • + +
    • ebusd_find
      + Execude find command on ebusd. Result will be used to display supported "set" and "get" commands. +

    • + + +
    • reading <reading-name>
      + Will read the actual value form ebusd and update the reading. +

    • + +
    • [r]~<class> <variable-name>~<comment>
      + Will read this variable from the ebusd and show the result as a popup.
      + Valid combinations are read from ebusd (using "get ebusd_find") and are selectable.
      +

    • + +
    +
    + + + Attributes +
      +
    • do_not_notify

    • +
    • disable

    • +
    • dummy

    • +
    • showtime

    • +
    • loglevel

    • +
    • ebusWritesEnabled 0,1
      + disable (0) or enable (1) that commands can be send to ebus devices
      + See also description for Set and Get
      + If Attribute is missing, default value is 0 (disable writes)
      +

    • +
    • Attributes of the format
      + [r|h]~<class>~<variable-name>~<comment>
      + 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.
      + If ebusd returns a list of semicolon separated values then several semicolon separated readings can be defined.
      + "dummy" is a placeholder for a reading that will be ignored. (e.g.: temperature;dummy;pressure).
      + The name of the reading can be suffixed by "<:number>" which is a multiplicator for + the evaluation within the specified interval. (eg. OutsideTemp:3 will evaluate this reading every 3-th cycle)
      + All text followed the reading seperated by a blank is given as an additional parameter to ebusd. + This can be used to request a single value if more than one is retrieved from ebus.
      +

    • +
    • Attributes of the format
      + [w]~<class>~<variable-name>~<comment from csv>
      + define parameters that can be changed on ebus devices (using the write command from ebusctl) + They will appear when they are defined by a "set" command as described above.
      + The value assigned to an attribute specifies the name that will be used in set to change a parameter for a ebus device.
      +

    • +
    • valueFormat
      + Defines a map to format values within GAEBUS.
      + All readings can be formated using syntax of sprinf. +
      + Example: { "temperature" => "%0.2f" } +

    • + +
    +
    +
+ +=end html +=cut