ComfoAir [devicename|none] [interval]"
+ if(@a < 3);
+
+ $hash->{BUSY} = 0;
+ $hash->{EXPECT} = "";
+
+ 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 $result = ComfoAir_ReadAnswer($hash, $getName, $msgHash->{replyCode});
+ 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 optiniert werden, sonst wird der 2. send ggf nicht vor einem Timeout gesendet ...
+ my $result = ComfoAir_ReadAnswer($hash, $setName, $setHash{$setName}{msgHash}{replyCode});
+ return "$setName -> $result";
+ }
+ 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";
+ readingsBeginUpdate($hash);
+ # Definition der einzelnen Felder abarbeiten
+ my @fields = unpack($p{"unpack"}, $data);
+ 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 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, 1)
+ 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;
+
+ if(!$hash->{QUEUE} || 0 == scalar(@{$hash->{QUEUE}})) {
+ $hash->{QUEUE} = [ \%entry ];
+ } 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} : "");
+
+ 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
+=begin html
+
+
+ComfoAir
+
+ ComfoAir provides a way to communicate with ComfoAir ventilation systems from Zehnder, especially the ComfoAir 350 (CA350).
+ It seems that many other ventilation systems use the same communication device and protocol,
+ e.g. WHR930 from StorkAir, G90-380 from Wernig and Santos 370 DC from Paul.
+ They are connected via serial line to the fhem computer.
+ This module is based on the protocol description at http://www.see-solutions.de/sonstiges/Protokollbeschreibung_ComfoAir.pdf
+ and copies some ideas from earlier modules for the same devices that were posted in the fhem forum from danhauck(Santos) and Joachim (WHR962).
+
+ The module can be used in two ways depending on how fhem and / or a vendor supplied remote control device
+ like CC Ease or CC Luxe are connected to the system. If a remote control device is connected it is strongly advised that
+ fhem does not send data to the ventilation system as well and only listens to the communication betweem the vendor equipment.
+ The RS232 interface used is not made to support more than two parties communicating and connecting fhem in parallel to a CC Ease or similar device can lead to
+ collisions when sending data which can corrupt the ventilation system.
+ If connected in parallel fhem should only passively listen and <Interval> is to be set to 0.
+ If no remote control device is connected to the ventilation systems then fhem has to take control and actively request data
+ in the interval to be defined. Otherwiese fhem will not see any data. In this case fhem can also send commands to modify settings.
+
+
+ Prerequisites
+
+
+ -
+ This module requires the Device::SerialPort or Win32::SerialPort module.
+
+
+
+
+
+ Define
+
+
+ define <name> ComfoAir <device> <Interval>
+
+ The module connects to the ventialation system through the given Device and either passively listens to data that is communicated
+ between the ventialation system and its remote control device (e.g. CC Luxe) or it actively requests data from the
+ ventilation system every <Interval> seconds
+
+ Example:
+
+ define ZL ComfoAir /dev/ttyUSB1@9600 60
+
+
+
+
+ Configuration of the module
+
+ apart from the serial connection and the interval which both are specified in the define command there are several attributes that
+ can optionally be used to modify the behavior of the module.
+ The module internally gives names to all the protocol messages that are defined in the module and these names can be used
+ in attributes to define which requests are periodically sent to the ventilation device. The same nams can also be used with
+ set commands to manually send a request. Since all messages and readings are generically defined in a data structure in the module, it should be
+ quite easy to add more protocol details if needed without programming.
+
+
+ The names currently defined are:
+
+
+ Bootloader-Version
+ Firmware-Version
+ RS232-Modus
+ Sensordaten
+ KonPlatine-Version
+ Verzoegerungen
+ Ventilation-Levels
+ Temperaturen
+ Betriebsstunden
+ Status-Bypass
+ Status-Vorheizung
+
+
+ The attributes that control which messages are sent / which data is requested every <Interval> seconds are:
+
+
+ poll-Bootloader-Version
+ poll-Firmware-Version
+ poll-RS232-Modus
+ poll-Sensordaten
+ poll-KonPlatine-Version
+ poll-Verzoegerungen
+ poll-Ventilation-Levels
+ poll-Temperaturen
+ poll-Betriebsstunden
+ poll-Status-Bypass
+ poll-Status-Vorheizung
+
+
+ if the attribute is set to 1, the corresponding data is requested every <Interval> seconds. If it is set to 0, then the data is not requested.
+ by default Ventilation-Levels, Temperaturen and Status-Bypass are requested if no attributes are set.
+
+ Example:
+
+ define ZL ComfoAir /dev/ttyUSB1@9600 60
+ attr ZL poll-Status-Bypass 0
+ define FileLog_Lueftung FileLog ./log/Lueftung-%Y.log ZL
+
+
+
+
+ Set-Commands
+
+ like with the attributes mentioned above, set commands can be used to send a request for data manually. The following set options are available for this:
+
+ request-Status-Bypass
+ request-Bootloader-Version
+ request-Sensordaten
+ request-Temperaturen
+ request-Firmware-Version
+ request-KonPlatine-Version
+ request-Ventilation-Levels
+ request-Verzoegerungen
+ request-Betriebsstunden
+ request-Status-Vorheizung
+
+ additionally important fields can be set:
+
+ Temp_Komfort (target temperature for comfort)
+ Stufe (ventilation level)
+
+
+
+ Get-Commands
+
+ All readings that are derived from the responses to protocol requests are also available as Get commands. Internally a Get command triggers the corresponding
+ request to the device and then interprets the data and returns the right field value. To avoid huge option lists in FHEMWEB, only the most important Get options
+ are visible in FHEMWEB. However this can easily be changed since all the readings and protocol messages are internally defined in the modue in a data structure
+ and to make a Reading visible as Get option only a little option (e.g. showget => 1 has to be added to this data structure
+
+
+ Attributes
+
+ - do_not_notify
+ - readingFnAttributes
+
+ - poll-Bootloader-Version
+ - poll-Firmware-Version
+ - poll-RS232-Modus
+ - poll-Sensordaten
+ - poll-KonPlatine-Version
+ - poll-Verzoegerungen
+ - poll-Ventilation-Levels
+ - poll-Temperaturen
+ - poll-Betriebsstunden
+ - poll-Status-Bypass
+ - poll-Status-Vorheizung
+ include a request for the data belonging to the named group when sending requests every interval seconds
+ - hide-Bootloader-Version
+ - hide-Firmware-Version
+ - hide-RS232-Modus
+ - hide-Sensordaten
+ - hide-KonPlatine-Version
+ - hide-Verzoegerungen
+ - hide-Ventilation-Levels
+ - hide-Temperaturen
+ - hide-Betriebsstunden
+ - hide-Status-Bypass
+ - hide-Status-Vorheizung
+ prevent readings of the named group from being created even if used passively without polling and an external remote control requests this data.
+ please note that this attribute doesn't delete already existing readings.
+ - queueDelay
+ modify the delay used when sending requests to the device from the internal queue, defaults to 1 second
+ - timeout
+ set the timeout for reads, defaults to 2 seconds
+
+
+
+
+=end html
+=cut
+