mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 12:49:34 +00:00
d52b398ed2
git-svn-id: https://svn.fhem.de/fhem/trunk@27160 2b470e98-0d58-463d-a4d8-8e2adae1ed80
1175 lines
49 KiB
Perl
Executable File
1175 lines
49 KiB
Perl
Executable File
##########################################################################
|
|
# $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 <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
# 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 <name> 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
|
|
|
|
<a name="ComfoAir"></a>
|
|
<h3>ComfoAir</h3>
|
|
<ul>
|
|
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).
|
|
<br>
|
|
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. <br>
|
|
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.
|
|
<br><br>
|
|
|
|
<b>Prerequisites</b>
|
|
<ul>
|
|
<br>
|
|
<li>
|
|
This module requires the Device::SerialPort or Win32::SerialPort module.
|
|
</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="ComfoAirDefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<br>
|
|
<code>define <name> ComfoAir <device> <Interval></code>
|
|
<br><br>
|
|
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 <br>
|
|
If <Interval> is set to 0 then no polling will be done and the module only listens to messages on the line.<br>
|
|
<br>
|
|
Example:<br>
|
|
<br>
|
|
<ul><code>define ZL ComfoAir /dev/ttyUSB1@9600 60</code></ul>
|
|
</ul>
|
|
<br>
|
|
|
|
<a name="ComfoAirConfiguration"></a>
|
|
<b>Configuration of the module</b><br><br>
|
|
<ul>
|
|
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. <br><br>
|
|
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.
|
|
|
|
<br>
|
|
The names currently defined are:
|
|
|
|
<pre>
|
|
Bootloader-Version
|
|
Firmware-Version
|
|
RS232-Modus
|
|
Sensordaten
|
|
KonPlatine-Version
|
|
Verzoegerungen
|
|
Ventilation-Levels
|
|
Temperaturen
|
|
Betriebsstunden
|
|
Status-Bypass
|
|
Status-Vorheizung
|
|
</pre>
|
|
|
|
The attributes that control which messages are sent / which data is requested every <Interval> seconds are:
|
|
|
|
<pre>
|
|
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
|
|
</pre>
|
|
|
|
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.
|
|
<br><br>
|
|
Example:<br><br>
|
|
<pre>
|
|
define ZL ComfoAir /dev/ttyUSB1@9600 60
|
|
attr ZL poll-Status-Bypass 0
|
|
define FileLog_Lueftung FileLog ./log/Lueftung-%Y.log ZL
|
|
</pre>
|
|
</ul>
|
|
|
|
<a name="ComfoAirSet"></a>
|
|
<b>Set-Commands</b><br>
|
|
<ul>
|
|
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:
|
|
<pre>
|
|
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
|
|
</pre>
|
|
additionally important fields can be set:
|
|
<pre>
|
|
Temp_Komfort (target temperature for comfort)
|
|
Stufe (ventilation level)
|
|
</pre>
|
|
</ul>
|
|
<a name="ComfoAirGet"></a>
|
|
<b>Get-Commands</b><br>
|
|
<ul>
|
|
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. <code>showget => 1</code> has to be added to this data structure
|
|
</ul>
|
|
<a name="ComfoAirattr"></a>
|
|
<b>Attributes</b><br><br>
|
|
<ul>
|
|
<li><a href="#do_not_notify">do_not_notify</a></li>
|
|
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
|
|
<br>
|
|
<li><b>poll-Bootloader-Version</b></li>
|
|
<li><b>poll-Firmware-Version</b></li>
|
|
<li><b>poll-RS232-Modus</b></li>
|
|
<li><b>poll-Sensordaten</b></li>
|
|
<li><b>poll-KonPlatine-Version</b></li>
|
|
<li><b>poll-Verzoegerungen</b></li>
|
|
<li><b>poll-Ventilation-Levels</b></li>
|
|
<li><b>poll-Temperaturen</b></li>
|
|
<li><b>poll-Betriebsstunden</b></li>
|
|
<li><b>poll-Status-Bypass</b></li>
|
|
<li><b>poll-Status-Vorheizung</b></li>
|
|
include a request for the data belonging to the named group when sending requests every interval seconds <br>
|
|
<li><b>hide-Bootloader-Version</b></li>
|
|
<li><b>hide-Firmware-Version</b></li>
|
|
<li><b>hide-RS232-Modus</b></li>
|
|
<li><b>hide-Sensordaten</b></li>
|
|
<li><b>hide-KonPlatine-Version</b></li>
|
|
<li><b>hide-Verzoegerungen</b></li>
|
|
<li><b>hide-Ventilation-Levels</b></li>
|
|
<li><b>hide-Temperaturen</b></li>
|
|
<li><b>hide-Betriebsstunden</b></li>
|
|
<li><b>hide-Status-Bypass</b></li>
|
|
<li><b>hide-Status-Vorheizung</b></li>
|
|
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.<br>
|
|
<li><b>queueDelay</b></li>
|
|
modify the delay used when sending requests to the device from the internal queue, defaults to 1 second <br>
|
|
<li><b>queueMax</b></li>
|
|
max length of the send queue, defaults to 50<br>
|
|
<li><b>timeout</b></li>
|
|
set the timeout for reads, defaults to 2 seconds <br>
|
|
<li><b>alignTime</b></li>
|
|
Aligns each periodic read request for the defined interval to this base time. This is typcally something like 00:00 (see the Fhem at command)
|
|
<br>
|
|
|
|
</ul>
|
|
<br>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|
|
|