########################################################################## # $Id$ # # fhem Modul für ComfoAir Lüftungsanlagen von Zehnder mit # serieller Schnittstelle (RS232) sowie dazu kompatible Anlagen wie # Storkair WHR 930, Storkair 950, Paul Santos 370 DC, Paul Santos 570 DC # Wernig G90-380, Wernig G90-550 # # Dieses Modul basiert auf der Protokollanalyse von SeeSolutions: # http://www.see-solutions.de/sonstiges/Protokollbeschreibung_ComfoAir.pdf # sowie auf den bereits existierenden Modulen von Joachim und danhauck # http://forum.fhem.de/index.php/topic,14697.0.html # # This file is part of fhem. # # Fhem is 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. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## # 2014-04-18 initial version # # todo: # - tests for interval timer and queue timer # - Timeout-Timer as feature in utils # package ComfoAir; use strict; use warnings; use GPUtils qw(:all); use Time::HiRes qw(gettimeofday time); use DevIo; use FHEM::HTTPMOD::Utils qw(:all); use Exporter ('import'); our @EXPORT_OK = qw(); our %EXPORT_TAGS = (all => [@EXPORT_OK]); BEGIN { GP_Import( qw( fhem CommandAttr CommandDeleteAttr addToDevAttrList AttrVal ReadingsVal ReadingsTimestamp readingsSingleUpdate readingsBeginUpdate readingsBulkUpdate readingsEndUpdate InternalVal makeReadingName Log3 RemoveInternalTimer InternalTimer deviceEvents EvalSpecials AnalyzePerlCommand CheckRegexp IsDisabled gettimeofday FmtDateTime GetTimeSpec fhemTimeLocal time_str2num min max minNum maxNum abstime2rel defInfo trim ltrim rtrim UntoggleDirect UntoggleIndirect IsInt fhemNc round sortTopicNum Svn_GetFile WriteFile DevIo_OpenDev DevIo_SimpleWrite DevIo_SimpleRead DevIo_CloseDev SetExtensions HttpUtils_NonblockingGet featurelevel defs modules attr init_done )); GP_Export( qw( Initialize )); }; my $Module_Version = '2.03 - 13.4.2021'; # %parseInfo: # replyCode => msgHashRef # msgHash => unpack, name, request, readings (array of readingHashes) # readingHash => name, map, set, setmin, setmax, hint, expr # jeder readingHash in parseMap wird in der Initialize-Funktion ergänzt um # - rmap aus map # - setopt aus map # - msgHash - Rückverweis auf msgHash my %parseInfo = ( "0002" => { unpack => "C", name => "Test-Modus-Ein", request => "0001", }, "001a" => { unpack => "C", name => "Test-Modus-Aus", request => "0019", }, "ff09" => { unpack => "C", name => "Klappen setzen", readings => [ { name => "Bypass", map => "1:offen, 0:geschlossen, 3:stop", set => "0009:%02x03", }]}, "000c" => { unpack => "CCS>S>", name => "Ventilation-Status", # PC Befehl request => "000b", readings => [ { name => "Proz_Zuluft"}, { name => "Proz_Abluft"}, { name => "UPM_Zuluft", expr => 'int(1875000/$val)'}, { name => "UPM_Abluft", expr => 'int(1875000/$val)'}]}, "0068" => { unpack => "CCCA*", name => "Bootloader-Version", # PC Befehl request => "0067", readings => [ { name => "Bootloader_Version_Major"}, { name => "Bootloader_Version_Minor"}, { name => "Bootloader_Version_Beta"}, { name => "Bootloader_Version_Name"}]}, "006a" => { unpack => "CCCA*", # PC Befehl name => "Firmware-Version", request => "0069", readings => [ { name => "Firmware_Version_Major"}, { name => "Firmware_Version_Minor"}, { name => "Firmware_Version_Beta"}, { name => "Firmware_Version_Name"}]}, "0098" => { unpack => "CCCCCCxCCCCCCCCCC", name => "Sensordaten", request => "0097", readings => [ { name => "Temp_Enthalpie", expr => '$val / 2 - 20'}, { name => "Feucht_Enthalpie"}, { name => "Analog1_Proz"}, { name => "Analog2_Proz"}, { name => "Koeff_Enthalpie"}, { name => "Timer_Enthalpie", expr => '$val * 12'}, { name => "Analog1_Zu_Wunsch"}, { name => "Analog1_Ab_Wunsch"}, { name => "Analog2_Zu_Wunsch"}, { name => "Analog2_Ab_Wunsch"}, { name => "Analog3_Proz"}, { name => "Analog4_Proz"}, { name => "Analog3_Zu_Wunsch"}, { name => "Analog3_Ab_Wunsch"}, { name => "Analog4_Zu_Wunsch"}, { name => "Analog4_Ab_Wunsch"}]}, "009c" => { unpack => "C", # PC Befehl name => "RS232-Modus", # eigener Request existiert nicht sondern set mit 9b erzeugt Antwort 9c readings => [ { name => "RS232-Modus", map => "0:Ende, 1:nur-PC, 2:nur-CC-Ease, 3:PC-Master, 4:PC-Log", set => "009b:%02x", }]}, "00a2" => { unpack => "CCA10CC", # PC Befehl name => "KonPlatine-Version", request => "00a1", readings => [ { name => "KonPlatine_Version_Major"}, { name => "KonPlatine_Version_Minor"}, { name => "KonPlatine_Version_Name"}, { name => "CC-Ease_Version"}, { name => "CC-Luxe_Version"}]}, "00ca" => { unpack => "CCCCCCCC", name => "Verzoegerungen", request => "00c9", readings => [ { name => "Verz_Bad_Einschalt"}, { name => "Verz_Bad_Ausschalt"}, { name => "Verz_L1_Ausschalt"}, { name => "Verz_Stosslueftung"}, { name => "Verz_Filter_Wochen"}, { name => "Verz_RF_Hoch_Kurz"}, { name => "Verz_RF_Hoch_Lang"}, { name => "Verz_Kuechenhaube_Ausschalt"}]}, "00ce" => { unpack => "CCCCCCCCCCCC", name => "Ventilation-Levels", request => "00cd", defaultpoll => 1, readings => [ { name => "Proz_Abluft_abwesend"}, { name => "Proz_Abluft_niedrig"}, { name => "Proz_Abluft_mittel"}, { name => "Proz_Zuluft_abwesend"}, { name => "Proz_Zuluft_niedrig"}, { name => "Proz_Zuluft_mittel"}, { name => "Proz_Abluft_aktuell"}, { name => "Proz_Zuluft_aktuell"}, { name => "Stufe", showget => 1, map => "0:auto, 1:abwesend, 2:niedrig, 3:mittel, 4:hoch", set => "0099:%02x"}, { name => "Zuluft_aktiv"}, { name => "Proz_Abluft_hoch"}, { name => "Proz_Zuluft_hoch"}]}, "00d2" => { unpack => "CCCCCCC", name => "Temperaturen", request => "00d1", defaultpoll => 1, check => '($fields[5] & 15) == 15', readings => [ { name => "Temp_Komfort", expr => '$val / 2 - 20', set => "00D3:%02x", setexpr => '($val + 20) *2', setmin => 12, setmax => 28, hint => "slider,12,1,28"}, { name => "Temp_Aussen" , showget => 1, expr => '$val / 2 - 20'}, { name => "Temp_Zuluft" , expr => '$val / 2 - 20'}, { name => "Temp_Abluft" , expr => '$val / 2 - 20'}, { name => "Temp_Fortluft", expr => '$val / 2 - 20'}, { name => "Temp_Flag"}, { name => "Temp_EWT", expr => '$val / 2 - 20'}]}, "00de" => { unpack => "H6H6H6S>S>S>S>H6", name => "Betriebsstunden", request => "00dd", readings => [ { name => "Betriebsstunden_Abwesend", expr => 'hex($val)'}, { name => "Betriebsstunden_Niedrig", expr => 'hex($val)'}, { name => "Betriebsstunden_Mittel", expr => 'hex($val)'}, { name => "Betriebsstunden_Frostschutz"}, { name => "Betriebsstunden_Vorheizung"}, { name => "Betriebsstunden_Bypass"}, { name => "Betriebsstunden_Filter"}, { name => "Betriebsstunden_Hoch", expr => 'hex($val)'}]}, "00e0" => { unpack => "xxCCCxC", name => "Status-Bypass", request => "00df", defaultpoll => 1, readings => [ { name => "Bypass_Faktor"}, { name => "Bypass_Stufe"}, { name => "Bypass_Korrektur"}, { name => "Bypass_Sommermodus", map => "0:nein, 1:ja"}]}, "00e2" => { unpack => "CCCS>C", name => "Status-Vorheizung", request => "00e1", readings => [ { name => "Status_Klappe", map => "0:geschlossen, 1:offen, 2:unbekannt"}, { name => "Status_Frostschutz", map => "0:inaktiv, 1:aktiv"}, { name => "Status_Vorheizung", map => "0:inaktiv, 1:aktiv"}, { name => "Frostminuten"}, # S> is 2 bytes as high low { name => "Status_Frostsicherheit", map => "1:extra, 4:sicher"}]}, ); my @setList; # helper to return valid set options if set is called with "?" my @getList; # helper to return valid get options if get is called with "?" my %setHash; # helper to reference the readings array in the above parseInfo for each set option my %getHash; # helper to reference the msgHash in parseInfo for each name / get option my %requestHash; # helper to reference each msgHash for each request Set my %cmdHash; # helper to map from send cmd code to msgHash of Reply my %AddSets = ( "SendRawData" => "" ); ##################################### sub Initialize { my $hash = shift; $hash->{ReadFn} = \&ComfoAir::ReadFn; $hash->{ReadyFn} = \&ComfoAir::ReadyFn; $hash->{DefFn} = \&ComfoAir::DefineFn; $hash->{UndefFn} = \&ComfoAir::UndefFn; $hash->{SetFn} = \&ComfoAir::SetFn; $hash->{GetFn} = \&ComfoAir::GetFn; $hash->{AttrFn} = \&ComfoAir::AttrFn; @setList = (); @getList = (); my @pollList = (); # ergänzt später $hash->{AttrList} # gehe durch alle Nachrichtentypen in parseInfo und erzeuge Hilfsdaten für set, get und die Attribute: # berechne reverse map aus der map zum Wandeln der gesetzten Werte # und berechnet setList für den "choose one of" Rückgabewert in det Set-Funktion # setHash enthält dann für jede gültige Set-Option eine Referenz auf den readingHash # requestHash: für jeden Request den Verweis auf msgHash innerhalb parseInfo while (my ($replyCode, $msgHashRef) = each (%parseInfo)) { my $msgName = $msgHashRef->{name}; # baue pollList und requestHash auf, setze Requests in @setList. if (defined ($msgHashRef->{request})) { # Nachricht kann abgefragt werden my $requestName = "request-" . $msgName; # für eine Set-Option my $attrName = "poll-" . $msgName; # für das Attribut zur Steuerung welche Blöcke abgefragt werden my $attr2Name = "hide-" . $msgName; # für das Attribut zum Verstecken von Blöcken $requestHash{$requestName} = $msgHashRef; # erzeuge requestHash für Verweis von requestName auf msgHash $requestHash{$requestName}->{replyCode} = $replyCode; # ergänze Replycode im msgHash $cmdHash{$msgHashRef->{request}} = $msgHashRef; # erzeuge %cmdHash für Verweis von RequestCode auf msgHash (für Debug Log) push @setList, $requestName; push @pollList, "$attrName:0,1"; push @pollList, "$attr2Name:0,1"; } # gehe durch alle Readings im Nachrichtentyp und erzeuge getHash, setHash und setList, rmap, setopt foreach my $readingHashRef (@{$msgHashRef->{readings}}) { my $reading = $readingHashRef->{name}; # Name des Readings # getHash erzeugen $getHash{$reading} = $readingHashRef; # erzeuge getHash mit Verweis von Reading-Name auf msgHash push @getList, $reading if ($readingHashRef->{showget}); # sichtbares get (alle Readings können per Get aktiv abgefragt werden) # Rückwärtsverweis auf msgHash erzeugen $readingHashRef->{msgHash} = $msgHashRef; # ergänze Rückwärtsverweis # gibt es für das Reading ein SET? if (defined($readingHashRef->{set})) { # ist eine Map definiert, aus der eine Reverse-Map und auch Hints abgeleitet werden können? if (defined($readingHashRef->{map})){ my $rm = $readingHashRef->{map}; $rm =~ s/([^ ,\$]+):([^ ,\$]+),? ?/$2 $1 /g; # reverse map string erzeugen my %rmap = split (' ', $rm); # reverse hash aus dem reverse string $readingHashRef->{rmap} = \%rmap; # reverse map im readingHash sichern my $hl = $readingHashRef->{map}; # create hint list from map $hl =~ s/([^ ,\$]+):([^ ,\$]+,?) ?/$2/g; $readingHashRef->{setopt} = $reading . ":$hl"; } else { $readingHashRef->{setopt} = $reading; # keine besonderen Optionen, nur den Namen für setopt verwenden. } if (defined($readingHashRef->{hint})){ # hints explizit definiert? (überschreibt evt. schon abgeleitete hints) $readingHashRef->{setopt} = $reading . ":" . $readingHashRef->{hint}; } $setHash{$reading} = $readingHashRef; # erzeuge Hash mit Verweis auf readingHashRef für jedes Reading mit Set push @setList, $readingHashRef->{setopt}; # speichere Liste mit allen Sets inkl. der Hints nach ":" für Rückgabe bei Set ? } } } $hash->{AttrList}= "do_not_notify:1,0 " . 'queueDelay ' . 'timeout ' . 'queueMax ' . 'alignTime ' . join (" ", @pollList) . " " . # Def der zyklisch abzufragenden Nachrichten $main::readingFnAttributes; return; } ##################################### sub DefineFn { my $hash = shift; # reference to the Fhem device hash my $def = shift; # definition string my @a = split( /[ \t]+/, $def ); # the above string split at space or tab my ($name, $ComfoAir, $dev, $interval) = @a; return "wrong syntax: define ComfoAir [devicename|none] [interval]" if(@a < 3); $hash->{BUSY} = 0; $hash->{EXPECT} = ''; $hash->{ModuleVersion} = $Module_Version; DevIo_CloseDev($hash); $hash->{DeviceName} = $dev; if($dev ne "none") { DevIo_OpenDev($hash, 0, 0); } if (!$interval) { $hash->{Interval} = 0; Log3 $name, 3, "$name: interval is 0 or not specified - not sending requests - just listening!"; } else { $hash->{Interval} = $interval; UpdateTimer($hash, \&ComfoAir::GetUpdate, 'start'); } Log3 $name, 3, "$name: Defined with device $dev" . ($interval ? ", interval $interval" : ''); return; } ##################################### sub UndefFn { my ($hash, $arg) = @_; my $name = $hash->{NAME}; DevIo_CloseDev($hash); RemoveInternalTimer ("timeout:".$name); StopQueueTimer($hash, {silent => 1}); UpdateTimer($hash, \&ComfoAir::GetUpdate, 'stop'); return; } ######################################################################### # Attr command # if validation fails, return something so CommandAttr in fhem.pl doesn't assign a value to $attr sub AttrFn { my $cmd = shift; # 'set' or 'del' my $name = shift; # the Fhem device name my $aName = shift; # attribute name my $aVal = shift // ''; # attribute value my $hash = $defs{$name}; # reference to the Fhem device hash Log3 $name, 5, "$name: attr $name $aName $aVal"; if ($cmd eq 'set') { if ($aName eq 'alignTime') { my ($alErr, $alHr, $alMin, $alSec, undef) = GetTimeSpec($aVal); return "Invalid Format $aVal in $aName : $alErr" if ($alErr); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); $hash->{'.TimeAlign'} = fhemTimeLocal($alSec, $alMin, $alHr, $mday, $mon, $year); UpdateTimer($hash, \&ComfoAir::GetUpdate, 'start'); # change timer for alignment } } elsif ($cmd eq 'del') { # Deletion of Attributes #Log3 $name, 5, "$name: del attribute $aName"; if ($aName eq 'alignTime') { delete $hash->{'.TimeAlign'}; } } return; } ##################################### sub GetFn { my @getValArr = @_; # rest is optional values my $hash = shift @getValArr; # reference to device hash my $name = shift @getValArr; # device name my $getName = shift @getValArr; # get option name my $getVal = join(' ', @getValArr); # optional value after get name return "\"get ComfoAir\" needs at least one argument" if(!$getName); if (!defined($getHash{$getName})) { # undefined Get Log3 $name, 5, "$name: Get $getName not found, return list @getList "; return "Unknown argument $getName, choose one of @getList "; } my $msgHash = $getHash{$getName}{msgHash}; # Hash für die Nachricht aus parseInfo Log3 $name, 5, "$name: Request found in getHash created from parseInfo data"; if (!$msgHash->{request}) { return "Protocol doesn't provide a command to get $getName"; } Send($hash, $msgHash->{request}, '', $msgHash->{replyCode}, 1); my ($err, $result) = ReadAnswer($hash, $getName, $msgHash->{replyCode}); return $err if ($err); return $result; } ##################################### sub SetFn { my ($hash, @a) = @_; return "\"set ComfoAir\" needs at least an argument" if(@a < 2); my $name = $hash->{NAME}; my ($cmd,$fmt,$data); my $setName = $a[1]; my $setVal = $a[2]; my $rawVal = ""; if (defined($requestHash{$setName})) { # set Option ist Daten-Abfrage-Request aus parseInfo Log3 $name, 5, "$name: Request found in requestHash created from parseInfo data"; Send($hash, $requestHash{$setName}{request}, "", $requestHash{$setName}{replyCode}); return ""; } if (defined($setHash{$setName})) { # set Option für einen einzelnen Wert, in parseInfo definiert -> generische Verarbeitung if (!defined($setVal)) { Log3 $name, 3, "$name: No Value given to set $setName"; return "No Value given to set $setName"; } Log3 $name, 5, "$name: Set found option $setName in setHash created from parseInfo data"; ($cmd, $fmt) = split(":", $setHash{$setName}{set}); # 1. Schritt, falls definiert per Umkehrung der Map umwandeln (z.B. Text in numerische Codes) if (defined($setHash{$setName}{rmap})) { if (defined($setHash{$setName}{rmap}{$setVal})) { # reverse map für das Reading und den Wert definiert $rawVal = $setHash{$setName}{rmap}{$setVal}; Log3 $name, 5, "$name: found $setVal in setHash rmap and converted to $rawVal"; } else { Log3 $name, 3, "$name: Set Value $setVal did not match defined map"; return "Set Value $setVal did not match defined map"; } } else { # wenn keine map, dann wenigstens sicherstellen, dass numerisch. if ($setVal !~ /^-?\d+\.?\d*$/) { Log3 $name, 3, "$name: Set Value $setVal is not numeric"; return "Set Value $setVal is not numeric"; } $rawVal = $setVal; } # 2. Schritt: falls definiert Min- und Max-Werte prüfen if (defined($setHash{$setName}{setmin})) { Log3 $name, 5, "$name: checking Value $rawVal against Min $setHash{$setName}{setmin}"; return "Set Value $rawVal is smaller than Min ($setHash{$setName}{setmin})" if ($rawVal < $setHash{$setName}{setmin}); } if (defined($setHash{$setName}{setmax})) { Log3 $name, 5, "$name: checking Value $rawVal against Max $setHash{$setName}{setmax}"; return "Set Value $rawVal is bigger than Max ($setHash{$setName}{setmax})" if ($rawVal > $setHash{$setName}{setmax}); } # 3. Schritt: Konvertiere mit setexpr falls definiert if (defined($setHash{$setName}{setexpr})) { my $val = $rawVal; $rawVal = eval($setHash{$setName}{setexpr}); ## no critic - expression needs to come from variable Log3 $name, 5, "$name: converted Value $val to $rawVal using expr $setHash{$setName}{setexpr}"; } # 4. Schritt: mit sprintf umwandeln und senden. $data = sprintf($fmt, $rawVal); # in parseInfo angegebenes Format bei set=> - meist Konvert in Hex Send($hash, $cmd, $data, 0); # Nach dem Set gleich den passenden Datenblock nochmals anfordern, damit die Readings de neuen Wert haben if ($setHash{$setName}{msgHash}{request}) { Send($hash, $setHash{$setName}{msgHash}{request}, "", $setHash{$setName}{msgHash}{replyCode},1); # falls ein minDelay bei Send implementiert wäre, müsste ReadAnswer optimiert werden, sonst wird der 2. send ggf nicht vor einem Timeout gesendet ... my ($err, $result) = ReadAnswer($hash, $setName, $setHash{$setName}{msgHash}{replyCode}); #return "$setName -> $result"; return $err if ($err); } return; } elsif (defined($AddSets{$setName})) { # Additional set option not defined in parseInfo but AddSets if($setName eq "SendRawData") { return "please specify data as cmd or cmd -> data in hex" if (!defined($setVal)); ($cmd, $data) = split("->",$setVal); # eingegebener Wert ist HexCmd -> HexData $data="" if(!defined($data)); } Send($hash, $cmd, $data, 0); } else { # undefiniertes Set Log3 $name, 5, "$name: Set $setName not found, return list @setList " . join (" ", keys %AddSets); return "Unknown argument $a[1], choose one of @setList " . join (" ", keys %AddSets); } return; } ##################################### # Called from the read functions sub ParseFrames { my $hash = shift; my $name = $hash->{NAME}; my $frame = $hash->{helper}{buffer}; $hash->{RAWBUFFER} = unpack ('H*', $frame); Log3 $name, 5, "$name: raw buffer: $hash->{RAWBUFFER}"; # check for full frame in buffer if ($frame =~ /\x07\xf0(.{3}(?:[^\x07]|(?:\x07\x07))*)\x07\x0f(.*)/s) { # got full frame (and maybe Ack before but that's ok) my $framedata = $1; $hash->{helper}{buffer} = $2; # only keep the rest after the frame $framedata =~ s/\x07\x07/\x07/g; # remove double x07 $hash->{LASTFRAMEDATA} = unpack ('H*', $framedata); Log3 $name, 5, "$name: ParseFrames got frame: $hash->{RAWBUFFER}" . " data $hash->{LASTFRAMEDATA} Rest " . unpack ('H*', $hash->{helper}{buffer}); return $framedata; } # ACK? elsif ($frame =~ /\x07\xf3(.*)/s) { my $level = ($hash->{Interval} ? 4 : 5); Log3 $name, $level, "$name: read got Ack"; $hash->{helper}{buffer} = $1; # only keep the rest after the frame if (!$hash->{EXPECT}) { $hash->{BUSY} = 0; # es wird keine weitere Antwort erwartet -> gleich weiter Send Queue abarbeiten und nicht auf alten Timer warten RemoveInternalTimer ("timeout:".$name); HandleSendQueue ("direct:".$name); # don't wait for next regular handle queue slot } } return; # continue reading, probably frame not fully received yet } ##################################### # Called from the read functions sub InterpretFrame { my $hash = shift; my $framedata = shift; my $name = $hash->{NAME}; my ($cmd, $hexcmd, $hexdata, $len, $data, $chk); if (defined($framedata)) { if ($framedata !~ /(.{2})(.)(.*)(.)/s) { Log3 $name, 3, "$name: read: error splitting frame into fields: $hash->{LASTFRAMEDATA}"; return; } $cmd = $1; $len = $2; $data = $3; $chk = unpack ('C', $4); $hexcmd = unpack ('H*', $cmd); $hexdata = unpack ('H*', $data); Log3 $name, 5, "$name: read split frame into cmd $hexcmd, len " . unpack ('C', $len) . ", data $hexdata chk $chk"; } # Länge prüfen if (unpack ('C', $len) != length($data)) { Log3 $name, 4, "$name: read: wrong length: " . length($data) . " (calculated) != " . unpack ('C', $len) . " (header)" . " cmd=$hexcmd, data=$hexdata, chk=$chk"; return; } # Checksum prüfen my $csum = unpack ('%8C*', $cmd . $len . $data . "\xad"); # berechne csum if($csum != $chk) { Log3 $name, 4, "$name: read: wrong checksum: $csum (calculated) != $chk (frame) cmd $hexcmd, data $hexdata"; return; }; # Parse Data if ($parseInfo{$hexcmd}) { if (!AttrVal($name, "hide-$parseInfo{$hexcmd}{name}", 0)) { # Definition für diesen Nachrichten-Typ gefunden my %p = %{$parseInfo{$hexcmd}}; Log3 $name, 4, "$name: read got " . $p{"name"} . " (reply code $hexcmd) with data $hexdata"; # Definition der einzelnen Felder abarbeiten my @fields = unpack($p{"unpack"}, $data); my $filter = 0; if ($p{check}) { my $result = eval($p{check}); ## no critic - expression needs to come from variable Log3 $name, 5, "$name: cmd $hexcmd check is " . $result . ', $fields[5] = ' . $fields[5] if ($fields[5] > 15); if (!$result) { Log3 $name, 5, "$name: filter data for failed check: @fields"; $filter = 1; } } if (!$filter) { readingsBeginUpdate($hash); for (my $i = 0; $i < scalar(@fields); $i++) { # einzelne Felder verarbeiten my $reading = $p{"readings"}[$i]{"name"}; my $val = $fields[$i]; # Exp zur Nachbearbeitung der Werte? if ($p{"readings"}[$i]{"expr"}) { Log3 $name, 5, "$name: read evaluate $val with expr " . $p{"readings"}[$i]{"expr"}; $val = eval($p{"readings"}[$i]{"expr"}); ## no critic - expr needs tocome from variable } # Map zur Nachbereitung der Werte? if ($p{"readings"}[$i]{"map"}) { my %map = split (/[,: ]+/, $p{"readings"}[$i]{"map"}); Log3 $name, 5, "$name: read maps value $val with " . $p{"readings"}[$i]{"map"}; $val = $map{$val} if ($map{$val}); } Log3 $name, 5, "$name: read assign $reading with $val"; readingsBulkUpdate($hash, $reading, $val); } readingsEndUpdate($hash, 1); } } } else { my $level = ($hash->{Interval} ? 4 : 5); Log3 $name, $level, "$name: read: unknown cmd $hexcmd, len " . unpack ('C', $len) . ", data $hexdata, chk $chk"; } if ($hash->{EXPECT}) { # der letzte Request erwartet eine Antwort -> ist sie das? if ($hexcmd eq $hash->{EXPECT}) { $hash->{BUSY} = 0; $hash->{EXPECT} = ''; Log3 $name, 5, "$name: read got expected reply ($hexcmd), setting BUSY=0"; } else { Log3 $name, 3, "$name: read did not get expected reply (" . $hash->{EXPECT} . ") but $hexcmd"; } } SendAck($hash) if ($hash->{Interval}); # todo: sowohl hier als auch am Ende von ParseFrames wird HandleSendQueue aufgerufen. Geht das nicht eleganter? if (!$hash->{EXPECT}) { # es wird keine Antwort mehr erwartet -> gleich weiter Send Queue abarbeiten und nicht auf Timer warten $hash->{BUSY} = 0; # zur Sicherheit falls ein Ack versäumt wurde RemoveInternalTimer ("timeout:".$name); HandleSendQueue ("direct:".$name); # don't wait for next regular handle queue slot } return; } ##################################### # Called from the global loop, when the select for hash->{FD} reports data sub ReadFn { my $hash = shift; my $name = $hash->{NAME}; my $buf; if ($hash->{DeviceName} eq 'none') { # simulate receiving if ($hash->{TestInput}) { $buf = $hash->{TestInput}; delete $hash->{TestInput}; } } else { $buf = DevIo_SimpleRead($hash); return if(!defined($buf)); } $hash->{helper}{buffer} .= $buf; # todo: does this loop really make sense? for (my $i = 0;$i < 2;$i++) { my $framedata = ParseFrames($hash); return if (!$framedata); InterpretFrame($hash, $framedata); } return; } ##################################### # Called from get / set to get a direct answer # todo: restructure this function (see Modbus) # handle BUSY when timeout here! sub ReadAnswer { my ($hash, $arg, $expectReply) = @_; my $name = $hash->{NAME}; return ("No FD", undef) if(!$hash || ($^O !~ /Win/ && !defined($hash->{FD}))); my ($buf, $framedata, $cmd); my $rin = ''; my $to = AttrVal($name, "timeout", 2); # default is 2 seconds timeout Log3 $name, 5, "$name: ReadAnswer called for get $arg"; for(;;) { if ($hash->{DeviceName} eq 'none') { # simulate receiving if ($hash->{TestInput}) { $buf = $hash->{TestInput}; delete $hash->{TestInput}; } else { #$hash->{BUSY} = 0; #$hash->{EXPECT} = ""; return ("Timeout reading answer for $arg", undef); } } elsif($^O =~ m/Win/ && $hash->{USBDev}) { $hash->{USBDev}->read_const_time($to*1000); # set timeout (ms) $buf = $hash->{USBDev}->read(999); if(length($buf) == 0) { Log3 $name, 3, "$name: Timeout in ReadAnswer for get $arg"; return ("Timeout reading answer for $arg", undef); } } else { if(!$hash->{FD}) { Log3 $name, 3, "$name: Device lost in ReadAnswer for get $arg"; return ("Device lost when reading answer for get $arg", undef); } vec($rin, $hash->{FD}, 1) = 1; # setze entsprechendes Bit in rin my $nfound = select($rin, undef, undef, $to); if($nfound < 0) { next if ($! == EAGAIN() || $! == EINTR() || $! == 0); my $err = $!; DevIo_Disconnected($hash); Log3 $name, 3, "$name: ReadAnswer $arg: error $err"; return("ReadAnswer $arg: $err", undef); } if($nfound == 0) { Log3 $name, 3, "$name: Timeout2 in ReadAnswer for $arg"; return ("Timeout reading answer for $arg", undef); } $buf = DevIo_SimpleRead($hash); if(!defined($buf)) { Log3 $name, 3, "$name: ReadAnswer for $arg got no data"; return ("No data", undef); } } if($buf) { $hash->{helper}{buffer} .= $buf; Log3 $name, 5, "$name: ReadAnswer got: " . unpack ("H*", $hash->{helper}{buffer}); } $framedata = ParseFrames($hash); if ($framedata) { InterpretFrame($hash, $framedata); $cmd = unpack ('H4x*', $framedata); if ($cmd eq $expectReply) { # das war's worauf wir gewartet haben Log3 $name, 5, "$name: ReadAnswer done with success"; return (undef, ReadingsVal($name, $arg, "")); } } HandleSendQueue("direct:".$name); } return; } ##################################### sub ReadyFn { my ($hash) = @_; return DevIo_OpenDev($hash, 1, undef) if($hash->{STATE} eq "disconnected"); # This is relevant for windows/USB only my $po = $hash->{USBDev}; if ($po) { my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; return ($InBytes>0); } return; } ##################################### sub GetUpdate { my $arg = shift; # called with a string type:$name my ($calltype, $name) = split(':', $arg); my $hash = $defs{$name}; UpdateTimer($hash, \&ComfoAir::GetUpdate, 'next'); foreach my $msgHashRef (values %parseInfo) { if (defined($msgHashRef->{request})) { my $default = ($msgHashRef->{defaultpoll} ? 1 : 0); # verwende als Defaultwert für Attribut, falls gesetzt in %parseInfo if (AttrVal($name, "poll-$msgHashRef->{name}", $default)) { Log3 $name, 5, "$name: GetUpdate requests $msgHashRef->{name}, default is $default"; Send($hash, $msgHashRef->{request}, "", $msgHashRef->{replyCode}); } } } return; } ##################################### sub Send { my ($hash, $hexcmd, $hexdata, $expectReply, $first) = @_; my $name = $hash->{NAME}; my $cmd = pack ('H*', $hexcmd); my $data = pack ('H*', $hexdata); my $len = pack ('C', length ($data)); my $csum = pack ('C', unpack ('%8C*', $cmd . $len . $data. "\xad")); my $framedata = $data.$csum; $framedata =~ s/\x07/\x07\x07/g; # double 07 in contents of frame including Checksum! my $frame = "\x07\xF0".$cmd.$len.$framedata."\x07\x0F"; my $hexframe = unpack ('H*', $frame); $expectReply = "" if (!$expectReply); Log3 $name, 4, "$name: send adds frame to queue with cmd $hexcmd" . ($cmdHash{$hexcmd} ? " (get " . $cmdHash{$hexcmd}{name} . ")" : "") . " / frame " . $hexframe; my %entry; $entry{DATA} = $frame; $entry{EXPECT} = $expectReply; my $qlen = ($hash->{QUEUE} ? scalar(@{$hash->{QUEUE}}) : 0); Log3 $name, 5, "$name: send queue length : $qlen"; if(!$qlen) { $hash->{QUEUE} = [ \%entry ]; } else { if ($qlen > AttrVal($name, "queueMax", 20)) { Log3 $name, 3, "$name: send queue too long, dropping request"; } else { if ($first) { unshift (@{$hash->{QUEUE}}, \%entry); } else { push(@{$hash->{QUEUE}}, \%entry); } } } HandleSendQueue("direct:".$name); return; } ####################################### sub TimeoutSend { my $param = shift; my (undef,$name) = split(':',$param); my $hash = $defs{$name}; Log3 $name, 3, "$name: timeout waiting for reply" . ($hash->{EXPECT} ? " expecting " . $hash->{EXPECT} : "") . " Request was " . $hash->{LASTREQUEST}; $hash->{BUSY} = 0; $hash->{EXPECT} = ""; return; } ####################################### sub HandleSendQueue { my $param = shift; my (undef,$name) = split(':',$param); my $hash = $defs{$name}; my $now = gettimeofday(); my $arr = $hash->{QUEUE}; my $qlen = ($arr ? scalar(@{ $arr }) : 0 ); Log3 $name, 5, "$name: HandleSendQueue called from " . FhemCaller() . ", qlen = $qlen"; StopQueueTimer($hash, {silent => 1}); if($qlen) { if (!$init_done) { # fhem not initialized, wait with IO StartQueueTimer($hash, \&ComfoAir::HandleSendQueue, {log => 'init not done, delay sending from queue'}); return; } if ($hash->{BUSY}) { # still waiting for reply to last request StartQueueTimer($hash, \&ComfoAir::HandleSendQueue, {log => 'send busy, delay writing from queue'}); return; } my $entry = $arr->[0]; my $bstring = $entry->{DATA}; my $hexcmd = unpack ('xxH4x*', $bstring); if($bstring ne "") { # if something to send - do so $hash->{LASTREQUEST} = unpack ('H*', $bstring); $hash->{BUSY} = 1; # at least wait for ACK Log3 $name, 4, "$name: handle queue sends" . ($cmdHash{$hexcmd} ? " get " . $cmdHash{$hexcmd}{name} : "") . " code: $hexcmd" . " frame: " . $hash->{LASTREQUEST} . ($entry->{EXPECT} ? " and wait for " . $entry->{EXPECT} : "") . ", V " . $hash->{ModuleVersion}; if ($hash->{DeviceName} eq 'none') { Log3 $name, 4, "$name: Simulate sending to none: " . unpack ('H*', $bstring); } else { DevIo_SimpleWrite($hash, $bstring, 0); } if ($entry->{EXPECT}) { # we expect a reply $hash->{EXPECT} = $entry->{EXPECT}; } my $to = AttrVal($name, "timeout", 2); # default is 2 seconds timeout RemoveInternalTimer ("timeout:".$name); InternalTimer($now+$to, "ComfoAir::TimeoutSend", "timeout:".$name, 0); } shift(@{$arr}); if(@{$arr} == 0) { # last item was sent -> delete queue delete($hash->{QUEUE}); } else { # more items in queue -> schedule next handle invocation StartQueueTimer($hash, \&ComfoAir::HandleSendQueue); } } return; } ####################################### sub SendAck { my $hash = shift; my $name = $hash->{NAME}; Log3 $name, 4, "$name: sending Ack"; DevIo_SimpleWrite($hash, "\x07\xf3", 0); return; } 1; =pod =item device =item summary module for Zehnder ComfoAir, StorkAir WHR930, Wernig G90-380 and Santos 370 =item summary_DE Modul für Zehnder ComfoAir, StorkAir WHR930, Wernig G90-380 and Santos 370 =begin html

ComfoAir

=end html =cut