############################################## # $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 . # ############################################################################## # Changelog: # # 2014-04-18 initial version # 2014-05-17 added more protocol commands, changed logging settings # 2014-05-25 added hide- attributes # 2014-07-07 corrected handling of 0xD2 Message (protocol is different than documented) # 2014-07-24 added max queue length checking and attribute # 2016-01-25 Reading Namen mit Umlauten korrigiert # 2016-04-06 Testmode Protokollbefehle hinzugefügt # 2016-11-13 korrektur bei set / get / readanswer. Set liefert bei Erfolg undef statt Text # 2017-02-12 Doku ergänzt # 2017-05-09 Text-Kodierung für summary korrigiert # # # Todo / Ideas: # package main; use strict; use warnings; use Time::HiRes qw( time ); sub ComfoAir_Initialize($); sub ComfoAir_Define($$); sub ComfoAir_Undef($$); sub ComfoAir_Set($@); sub ComfoAir_Get($@); sub ComfoAir_Read($); sub ComfoAir_Ready($); sub ComfoAir_ReadAnswer($$$); sub ComfoAir_GetUpdate($$); sub ComfoAir_Send($$$;$$); sub ComfoAir_ParseFrames($); sub ComfoAir_InterpretFrame($$); sub ComfoAir_HandleSendQueue($); sub ComfoAir_SendAck($); sub ComfoAir_TimeoutSend($); my $ComfoAir_Version = '1.51 - 9.5.2017'; # %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 %ComfoAir_AddSets = ( "SendRawData" => "" ); ##################################### sub ComfoAir_Initialize($) { my ($hash) = @_; require "$attr{global}{modpath}/FHEM/DevIo.pm"; $hash->{ReadFn} = "ComfoAir_Read"; $hash->{ReadyFn} = "ComfoAir_Ready"; $hash->{DefFn} = "ComfoAir_Define"; $hash->{UndefFn} = "ComfoAir_Undef"; $hash->{SetFn} = "ComfoAir_Set"; $hash->{GetFn} = "ComfoAir_Get"; @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 " . #"minSendDelay " . join (" ", @pollList) . " " . # Def der zyklisch abzufragenden Nachrichten $readingFnAttributes; } ##################################### sub ComfoAir_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my ($name, $ComfoAir, $dev, $interval) = @a; return "wrong syntax: define ComfoAir [devicename|none] [interval]" if(@a < 3); $hash->{BUSY} = 0; $hash->{EXPECT} = ""; $hash->{ModuleVersion} = $ComfoAir_Version; if (!defined($interval)) { $hash->{INTERVAL} = 0; Log 1, "$name: interval is 0 or not defined - not sending requests - just listening!"; } else { $hash->{INTERVAL} = $interval; } DevIo_CloseDev($hash); if($dev eq "none") { Log 1, "$name: device is none, commands will be echoed only"; return undef; } $hash->{DeviceName} = $dev; my $ret = DevIo_OpenDev($hash, 0, 0); InternalTimer(gettimeofday()+1, "ComfoAir_GetUpdate", $hash, 0) # erste Abfrage von Werten nach 1 Sekunde (zumindest in Queue stellen) if ($hash->{INTERVAL}); return $ret; } ##################################### sub ComfoAir_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; DevIo_CloseDev($hash); RemoveInternalTimer ("timeout:".$name); RemoveInternalTimer ("queue:".$name); RemoveInternalTimer ($hash); return undef; } ##################################### sub ComfoAir_Get($@) { my ($hash, @a) = @_; return "\"get ComfoAir\" needs at least one argument" if(@a < 2); my $name = $hash->{NAME}; my $getName = $a[1]; if (defined($getHash{$getName})) { # get Option für Reading aus parseInfo -> generische Verarbeitung my $msgHash = $getHash{$getName}{msgHash}; # Hash für die Nachricht aus parseInfo Log3 $name, 3, "$name: Request found in getHash created from parseInfo data"; if ($msgHash->{request}) { ComfoAir_Send($hash, $msgHash->{request}, "", $msgHash->{replyCode}, 1); my ($err, $result) = ComfoAir_ReadAnswer($hash, $getName, $msgHash->{replyCode}); return $err if ($err); return $result; } else { return "Protocol doesn't provide a command to get $getName"; } } else { # undefiniertes Get Log3 $name, 5, "$name: Get $getName not found, return list @getList "; return "Unknown argument $a[1], choose one of @getList "; } return undef; } ##################################### sub ComfoAir_Set($@) { 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"; ComfoAir_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}); 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 ComfoAir_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}) { ComfoAir_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) = ComfoAir_ReadAnswer($hash, $setName, $setHash{$setName}{msgHash}{replyCode}); #return "$setName -> $result"; return $err if ($err); } return undef; } elsif (defined($ComfoAir_AddSets{$setName})) { # Additional set option not defined in parseInfo but ComfoAir_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)); } ComfoAir_Send($hash, $cmd, $data, 0); } else { # undefiniertes Set Log3 $name, 5, "$name: Set $setName not found, return list @setList " . join (" ", keys %ComfoAir_AddSets); return "Unknown argument $a[1], choose one of @setList " . join (" ", keys %ComfoAir_AddSets); } return undef; } ##################################### # Called from the read functions sub ComfoAir_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; } 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); RemoveInternalTimer ("queue:".$name); ComfoAir_HandleSendQueue ("direct:".$name); # don't wait for next regular handle queue slot } return undef; } else { return undef; # continue reading, probably frame not fully received yet } } ##################################### # Called from the read functions sub ComfoAir_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) { $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"; } else { Log3 $name, 3, "$name: read: error splitting frame into fields: $hash->{LASTFRAMEDATA}"; return; } } # 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}) { Log3 $name, 5, "$name: cmd $hexcmd check is " . eval($p{check}) . ', $fields[5] = ' . $fields[5] if ($fields[5] > 15); if (!eval($p{check})) { 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"}); } # 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"; } } ComfoAir_SendAck($hash) if ($hash->{INTERVAL}); 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); RemoveInternalTimer ("queue:".$name); ComfoAir_HandleSendQueue ("direct:".$name); # don't wait for next regular handle queue slot } } ##################################### # Called from the global loop, when the select for hash->{FD} reports data sub ComfoAir_Read($) { my $hash = shift; my $name = $hash->{NAME}; my $buf = DevIo_SimpleRead($hash); return if(!defined($buf)); $hash->{helper}{buffer} .= $buf; for (my $i = 0;$i < 2;$i++) { my $framedata = ComfoAir_ParseFrames($hash); return if (!$framedata); ComfoAir_InterpretFrame($hash, $framedata); } } ##################################### # Called from get / set to get a direct answer sub ComfoAir_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($^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("ComfoAir_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 = ComfoAir_ParseFrames($hash); if ($framedata) { ComfoAir_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, "")); } } ComfoAir_HandleSendQueue("direct:".$name); } } ##################################### sub ComfoAir_Ready($) { my ($hash) = @_; return DevIo_OpenDev($hash, 1, undef) if($hash->{STATE} eq "disconnected"); # This is relevant for windows/USB only my $po = $hash->{USBDev}; my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; return ($InBytes>0); } ##################################### sub ComfoAir_GetUpdate($$) { my ($hash) = @_; my $name = $hash->{NAME}; InternalTimer(gettimeofday()+$hash->{INTERVAL}, "ComfoAir_GetUpdate", $hash, 0) if ($hash->{INTERVAL}); 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"; ComfoAir_Send($hash, $msgHashRef->{request}, "", $msgHashRef->{replyCode}); } } } } ##################################### sub ComfoAir_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); } } } ComfoAir_HandleSendQueue("direct:".$name); } ####################################### sub ComfoAir_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} = ""; }; ####################################### sub ComfoAir_HandleSendQueue($) { my $param = shift; my (undef,$name) = split(':',$param); my $hash = $defs{$name}; my $arr = $hash->{QUEUE}; my $now = gettimeofday(); my $queueDelay = AttrVal($name, "queueDelay", 1); Log3 $name, 5, "$name: handle send queue"; if(defined($arr) && @{$arr} > 0) { if (!$init_done) { # fhem not initialized, wait with IO RemoveInternalTimer ("queue:".$name); InternalTimer($now+$queueDelay, "ComfoAir_HandleSendQueue", "queue:".$name, 0); Log3 $name, 3, "$name: init not done, delay writing from queue"; return; } if ($hash->{BUSY}) { # still waiting for reply to last request RemoveInternalTimer ("queue:".$name); InternalTimer($now+$queueDelay, "ComfoAir_HandleSendQueue", "queue:".$name, 0); Log3 $name, 5, "$name: 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}; 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 RemoveInternalTimer ("queue:".$name); InternalTimer($now+$queueDelay, "ComfoAir_HandleSendQueue", "queue:".$name, 0); } } } ####################################### sub ComfoAir_SendAck($) { my $hash = shift; my $name = $hash->{NAME}; Log3 $name, 4, "$name: sending Ack"; DevIo_SimpleWrite($hash, "\x07\xf3", 0); } 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