diff --git a/fhem/FHEM/98_AirUnit.pm b/fhem/FHEM/98_AirUnit.pm new file mode 100644 index 000000000..1f7ec144e --- /dev/null +++ b/fhem/FHEM/98_AirUnit.pm @@ -0,0 +1,1154 @@ +# $Id$ +############################################################################## +# +# 98_AirUnit.pm +# An FHEM Perl module for controlling Danfoss AirUnits (a1,a2,w1,w2). +# +# Copyright by René Dommerich & Ulf von Mersewsky +# e-mail: rdommerich at gmx punkt com +# e-mail: umersewsky at gmail punkt com +# +# 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/>. +# +############################################################################## + +package FHEM::AirUnit; + +use GPUtils qw(:all); +use strict; +use warnings; + +require DevIo; + +BEGIN { + GP_Import( qw( + AttrVal + CommandAttr + readingsSingleUpdate + readingsBeginUpdate + readingsBulkUpdate + readingsEndUpdate + readingFnAttributes + ReadingsVal + Log3 + gettimeofday + InternalVal + InternalTimer + RemoveInternalTimer + TimeNow + time_str2num + Value + )); +}; + +GP_Export( + qw( + Initialize + ) +); + +my $Version = '0.0.5.9 - Jul 2023'; + +####################### GET Paramter ################################################ +# Das sind die Zahlen die gesendet werden müssen, damit man die Informationen erhält. +##################################################################################### + +my @OUTDOOR_TEMPERATURE = (0x01, 0x04, 0x03, 0x34); #### REGISTER_1_READ, OUTDOOR_TEMPERATURE +my @ROOM_TEMPERATURE = (0x01, 0x04, 0x03, 0x00); #### REGISTER_1_READ, ROOM_TEMPERATURE +my @SUPPLY_TEMPERATURE = (0x01, 0x04, 0x14, 0x73); #### REGISTER_1_READ, SUPPLY_TEMPERATURE / ZULUFT +my @EXTRACT_TEMPERATURE = (0x01, 0x04, 0x14, 0x74); #### REGISTER_1_READ, EXTRACT_TEMPERATURE / ABLUFT +my @EXHAUST_TEMPERATURE = (0x01, 0x04, 0x14, 0x75); #### REGISTER_1_READ, EXHAUST_TEMPERATURE + +my @HUMIDITY = (0x01, 0x04, 0x14, 0x70); #### REGISTER_1_READ, HUMIDITY +my @FAN_SPEED_SUPPLY = (0x04, 0x04, 0x14, 0x28); #### REGISTER_1_READ, FAN_SPEED_SUPPLY +my @FAN_SPPED_EXTRACT = (0x04, 0x04, 0x14, 0x29); #### REGISTER_1_READ, FAN_SPPED_EXTRACT +my @AIR_INPUT = (0x01, 0x04, 0x14, 0x40); #### REGISTER_1_READ, AIR_INPUT +my @AIR_OUTPUT = (0x01, 0x04, 0x14, 0x41); #### REGISTER_1_READ, AIR_OUTPUT +my @BATTERY_LIFE = (0x01, 0x04, 0x03, 0x0f); #### REGISTER_1_READ, BATTERY_LIFE +my @FILTER_LIFE = (0x01, 0x04, 0x14, 0x6a); #### REGISTER_1_READ, FILTER_LIFE + +my @BOOST = (0x01, 0x04, 0x15, 0x30); #### REGISTER_1_READ, BOOST ON/OFF +my @BOOST_AUTOMATIC = (0x01, 0x04, 0x17, 0x02); #### REGISTER_1_READ, BOOST_AUTOMATIC ON/OFF +my @BOOST_DURATION = (0x01, 0x04, 0x15, 0x31); #### REGISTER_1_READ, BOOST_DURATION +my @BYPASS = (0x01, 0x04, 0x14, 0x60); #### REGISTER_1_READ, BYPASS +my @BYPASS_AUTOMATIC = (0x01, 0x04, 0x17, 0x06); #### REGISTER_1_READ, BYPASS_AUTOMATIC ON/OFF +my @BYPASS_DURATION = (0x01, 0x04, 0x14, 0x62); #### REGISTER_1_READ, BYPASS_DURATION + +my @NIGHTCOOLING = (0x01, 0x04, 0x15, 0x71); #### REGISTER_1_READ, NIGHTCOOLING ON/OFF +my @FIREPLACE = (0x01, 0x04, 0x17, 0x07); #### REGISTER_1_READ, FIREPLACE ON/OFF +#my @COOKERHOOD = (0x01, 0x04, 0x15, 0x34); #### REGISTER_1_READ, COOKERHOOD ON/OFF + +my @MODE = (0x01, 0x04, 0x14, 0x12); #### REGISTER_1_READ, MODE +my @FAN_STEP = (0x01, 0x04, 0x15, 0x61); #### REGISTER_1_READ, FANSPEED / FANSTUFE in MANUELL - MODE + +my @FANSPEED_IN_RPM = (0x04, 0x04, 0x14, 0x50); #### REGISTER_4_READ, FANSPEED_IN_RPM +my @FANSPEED_OUT_RPM = (0x04, 0x04, 0x14, 0x51); #### REGISTER_4_READ, FANSPEED_OUT_RPM + +my @MODEL = (0x01, 0x04, 0x15, 0xe5); #### REGISTER_1_READ, MODEL +my @MODEL_SN = (0x04, 0x04, 0x00, 0x25); #### REGISTER_4_READ, MODEL SERIALNUMBER +my @OPERATION_TIME = (0x00, 0x04, 0x03, 0xe0); #### REGISTER_0_READ, OPERATION_TIME + +####################### SET Paramter ################################################################## +# Das sind die Zahlen die gesendet werden müssen + eine 5. (die Option), damit man etwas bewirken kann. +####################################################################################################### + +my @W_BOOST = (0x01, 0x06, 0x15, 0x30); #### REGISTER_1_WRITE, BOOST ON/OFF +my @W_BYPASS = (0x01, 0x06, 0x14, 0x60); #### REGISTER_1_WRITE, BYPASS ON/OFF +my @W_NIGHTCOOLING = (0x01, 0x06, 0x15, 0x71); #### REGISTER_1_WRITE, NIGHTCOOLING ON/OFF +my @W_DISABLE_BOOST_AUTOMATIC = (0x01, 0x06, 0x17, 0x02); #### REGISTER_1_WRITE, BOOST_AUTOMATIC ON/OFF +my @W_DISABLE_BYPASS_AUTOMATIC = (0x01, 0x06, 0x17, 0x06); #### REGISTER_1_WRITE, BYPASS_AUTOMATIC ON/OFF +my @W_MODE = (0x01, 0x06, 0x14, 0x12); #### REGISTER_1_WRITE, MODE +my @W_FAN_STEP = (0x01, 0x06, 0x15, 0x61); #### REGISTER_1_WRITE, FAN_STEP +my @W_FIREPLACE = (0x01, 0x06, 0x17, 0x07); #### REGISTER_1_WRITE, FIREPLACE ON/OFF +#my @W_COOKERHOOD = (0x01, 0x06, 0x15, 0x34); #### REGISTER_1_WRITE, COOKERHOOD ON/OFF +my @W_BOOST_DURATION = (0x01, 0x06, 0x15, 0x31); #### REGISTER_1_READ, BOOST_DURATION +my @W_BYPASS_DURATION = (0x01, 0x06, 0x14, 0x62); #### REGISTER_1_READ, BYPASS_DURATION + + +######################################## + +sub Initialize() +{ + my ($hash) = @_; + + $hash->{DefFn} = \&Define; # definiert das Gerät + $hash->{UndefFn} = \&Undefine; # legt fest, was alles mein löschen gemacht wird + $hash->{GetFn} = \&Get; # nicht wirklich benötigt, eher ein TEST, viell. fällt mir noch was ein + $hash->{SetFn} = \&Set; # dient zum setzen der SET Paramter + $hash->{ReadFn} = \&Read; # wird von DevIO beim Nachrichteneingang gerufen + $hash->{ReadyFn} = \&Ready; # wird von DevIO bei Kommunikationsproblemen gerufen + $hash->{AttrFn} = \&Attr; # nur kopiert und angepasst + $hash->{AttrList} = "disable:0,1 ". + $readingFnAttributes; + + return; +} + +######################################## + +sub Define(){ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "Usage: define <name> AirUnit <ip-address:port> [poll-interval]" + if(@a <3 || @a >4); + + my $name = $a[0]; + my $host = $a[2]; + my $port = 30046; + + my $interval = 5*60; + $interval = $a[3] if(@a == 4); + $interval = 30 if( $interval < 30 ); + + $hash->{NAME} = $name; + $hash->{ModuleVersion} = $Version; + + $hash->{STATE} = "Initializing"; + if ( $host =~ /(.*):(.*)/xms ) { + $host = $1; + $port = $2; + } + $hash->{INTERVAL} = $interval; + $hash->{NOTIFYDEV} = "global"; + $hash->{DeviceName} = join(':', $host, $port); + $hash->{devioLoglevel} = 4; + + $hash->{helper}{commandQueue} = []; + + InitCommands($hash); + + ::DevIo_CloseDev($hash) if ( ::DevIo_IsOpen($hash) ); + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+2, \&GetUpdate, $hash, 0); + + return; +} + +######################################## + +sub Undefine() { + my ($hash, $arg) = @_; + + RemoveInternalTimer($hash); # Timer wird gelöscht + ::DevIo_CloseDev($hash); + + return; +} + +######################################## +# called repeatedly if device disappeared + +sub Ready() +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + # reset command queue + $hash->{helper}{commandQueue} = []; + + # try to reopen the connection in case the connection is lost and Attribute disable = 0 + if (AttrVal($name, "disable", 0) == 0) { + return ::DevIo_OpenDev($hash, 1, undef, \&sendNextRequest); + } + + return; +} + +######################################## +# called when data was received + +sub Read() +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $commands = $hash->{helper}{commandHash}; + + # read the available data + my $buf = ::DevIo_SimpleRead($hash); + + # stop processing if no data is available (device disconnected) + return if(!defined($buf)); + + Log3($name, 4, "AirUnit $name: received: ".unpack('H*', $buf)); + + my $lastCmd = InternalVal($name, 'LastCommand', ''); + + if ($lastCmd =~ /0106.*/xms && $buf =~ /\x00{63}/xms) { + # received answer from Set command + Log3($name, 4, "AirUnit $name: command performed: $lastCmd"); + } elsif (defined($commands->{$lastCmd})) { + $commands->{$lastCmd}->($hash, $buf); + } else { + Log3($name, 4, "AirUnit $name: handling of command not defined: $lastCmd"); + } + + sendNextRequest($hash); + + return; +} + +######################################## + +sub Get() { + my ($hash, $name, $cmd, @val) = @_; + + if ($cmd ne '?' && AttrVal($name, 'disable', 0) == 1) { + Log3($name, 3, "AirUnit $name: disabled, ignore : get $name $cmd"); + return; + } elsif($cmd eq 'update') { + DoUpdate($hash); + return; + } + + my $list = " update:noArg"; + + return "Unknown argument $cmd, choose one of $list"; +} + +######################################## + +sub Set() { + my ($hash, $name, $cmd, $val) = @_; + my @w_settings; + + if ($cmd ne '?' && AttrVal($name, 'disable', 0) == 1) { + Log3($name, 3, "AirUnit $name: disabled, ignore : set $name $cmd $val"); + return; + } elsif($cmd eq 'Modus') { + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "Bedarfsmodus"){ + @w_settings = (@W_MODE, 0x00); + }elsif($val eq "Programm"){ + @w_settings = (@W_MODE, 0x01); + }elsif($val eq "Manuell"){ + @w_settings = (@W_MODE, 0x02); + }elsif($val eq "Aus"){ + @w_settings = (@W_MODE, 0x03); + }else { + return "Fehlerhafter Paramter: set $name $cmd $val"; + } + DoChange($hash, \@w_settings, \@MODE); + return; + } + elsif ($cmd eq 'Luefterstufe') { + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + my $myMode = ReadingsVal($name, "Modus" , ""); + Log3($name, 3, "AirUnit $name: Modus: $myMode"); + if (($val <= 10 || $val >= 1) and $myMode eq "Manuell"){ + @w_settings = (@W_FAN_STEP, $val); + DoChange($hash, \@w_settings, \@FAN_STEP); + }else{ + return "Lueftung ist nicht im manuellen Modus."; + } + return; + } + elsif ($cmd eq 'Stosslueftung'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_BOOST, 0x01); + }elsif($val eq "off"){ + @w_settings = (@W_BOOST, 0x00); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BOOST); + return; + } + elsif ($cmd eq 'Bypass'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_BYPASS, 0x01); + }elsif($val eq "off"){ + @w_settings = (@W_BYPASS, 0x00); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BYPASS); + return; + } + elsif ($cmd eq 'Nachtkuehlung'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_NIGHTCOOLING, 0x01); + }elsif($val eq "off"){ + @w_settings = (@W_NIGHTCOOLING, 0x00); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@NIGHTCOOLING); + return; + } + elsif ($cmd eq 'Feuerstaette'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_FIREPLACE, 0x01); + }elsif($val eq "off"){ + @w_settings = (@W_FIREPLACE, 0x00); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@FIREPLACE); + return; + } + # elsif ($cmd eq 'Dunstabzugshaube'){ + # Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + # if($val eq "on"){ + # @w_settings = (@W_COOKERHOOD, 0x01); + # }elsif($val eq "off"){ + # @w_settings = (@W_COOKERHOOD, 0x00); + # }else { + # return "Fehlerhafter Paramter $val für Setting $cmd\n"; + # } + # #setONOFF($hash, @w_settings); + # sendRequest($hash, @w_settings); + # return undef; + # } + elsif ($cmd eq 'automatische_Stosslueftung'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_DISABLE_BOOST_AUTOMATIC, 0x00); + }elsif($val eq "off"){ + @w_settings = (@W_DISABLE_BOOST_AUTOMATIC, 0x01); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BOOST_AUTOMATIC); + return; + } + elsif ($cmd eq 'automatischer_Bypass'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val eq "on"){ + @w_settings = (@W_DISABLE_BYPASS_AUTOMATIC, 0x00); + }elsif($val eq "off"){ + @w_settings = (@W_DISABLE_BYPASS_AUTOMATIC, 0x01); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BYPASS_AUTOMATIC); + return; + } + elsif ($cmd eq 'Stosslueftung_Dauer'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val <= 23 || $val >= 1){ + @w_settings = (@W_BOOST_DURATION, $val); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BOOST_DURATION); + return; + } + elsif ($cmd eq 'Bypass_Dauer'){ + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + if($val <= 23 || $val >= 1){ + @w_settings = (@W_BYPASS_DURATION, $val); + }else { + return "Fehlerhafter Paramter $val für Setting $cmd\n"; + } + DoChange($hash, \@w_settings, \@BYPASS_DURATION); + return; + } + elsif($cmd eq 'Intervall' && defined($val) ) { + Log3($name, 3, "AirUnit $name: set $name $cmd $val"); + $val = 30 if( $val < 30 ); + $hash->{INTERVAL} = $val; + return "Intervall wurde auf $val Sekunden gesetzt."; + } + + my $list = " Modus:Bedarfsmodus,Programm,Manuell,Aus " + ." Luefterstufe:slider,1,1,10 " + ." Stosslueftung:on,off " + ." Bypass:on,off " + ." Nachtkuehlung:on,off " + ." Feuerstaette:on,off " +# ." Dunstabzugshaube:on,off " + ." automatische_Stosslueftung:on,off " + ." automatischer_Bypass:on,off " + ." Stosslueftung_Dauer:slider,1,1,23 " + ." Bypass_Dauer:slider,1,1,23 " + ." Intervall"; + + return "Unknown argument $cmd, choose one of $list"; +} + +######################################## + +sub Attr() { + + my ($cmd,$name,$aName,$aVal) = @_; + my $hash = $::defs{$name}; + + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + if ($cmd eq 'set') { + if ($aName eq 'disable') { + if ($aVal == 0) { + readingsSingleUpdate($hash, 'state', 'Initializing', 1) if (Value($name) ne 'opened'); + } elsif ($aVal == 1) { + readingsSingleUpdate($hash, 'state', 'disabled', 1); + } else { + return "Invalid allowSetParameter $aVal"; + } + } + } + + if ($cmd eq 'del') { + if ($aName eq 'disable' && AttrVal($name, $aName, 0) == 1) { + readingsSingleUpdate($hash, 'state', 'Initializing', 1); + } + } + + return; +} + +######################################## + +sub GetUpdate() { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $interval = $hash->{INTERVAL}; + + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+$interval, \&GetUpdate, $hash, 0); + return if( AttrVal($name, "disable", 0 ) == 1 ); + + DoUpdate($hash); + + return; +} + +######################################## + +sub DoUpdate(){ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $lastCmdTs = $hash->{LastCommandTS}; + my $queueRef = $hash->{helper}{commandQueue}; + my $orgQueueCount = @$queueRef; + + my $elapsedTime = int(gettimeofday() - time_str2num($lastCmdTs)); + if($orgQueueCount > 0 && $elapsedTime > $hash->{INTERVAL} * 2) { + Log3($name, 3, "AirUnit $name: reset command queue, timeout after $elapsedTime seconds"); + $queueRef = []; + $orgQueueCount = 0; + } + + # Update readings + + # get_Temperature_Value + + push(@$queueRef, \@OUTDOOR_TEMPERATURE); + push(@$queueRef, \@ROOM_TEMPERATURE); + push(@$queueRef, \@SUPPLY_TEMPERATURE); + push(@$queueRef, \@EXTRACT_TEMPERATURE); + push(@$queueRef, \@EXHAUST_TEMPERATURE); + + # get_Value_in_Percent + + push(@$queueRef, \@HUMIDITY); + push(@$queueRef, \@AIR_INPUT); + push(@$queueRef, \@AIR_OUTPUT); + push(@$queueRef, \@FAN_SPEED_SUPPLY); + push(@$queueRef, \@FAN_SPPED_EXTRACT); + + # get_Lifetimes_in_Percent + + push(@$queueRef, \@FILTER_LIFE); + push(@$queueRef, \@BATTERY_LIFE); + + # get_ON_or_OFF_Value + + push(@$queueRef, \@BOOST); + push(@$queueRef, \@BYPASS); + push(@$queueRef, \@NIGHTCOOLING); + push(@$queueRef, \@FIREPLACE); + push(@$queueRef, \@BOOST_AUTOMATIC); + push(@$queueRef, \@BYPASS_AUTOMATIC); + + # get_FanSpeed_in_RPM + + push(@$queueRef, \@FANSPEED_IN_RPM); + push(@$queueRef, \@FANSPEED_OUT_RPM); + + # get_Value + + push(@$queueRef, \@MODE); + push(@$queueRef, \@FAN_STEP); + push(@$queueRef, \@BOOST_DURATION); + push(@$queueRef, \@BYPASS_DURATION); + push(@$queueRef, \@OPERATION_TIME); + + #get_String + + push(@$queueRef, \@MODEL) if (InternalVal($name, 'Model', '') eq ''); + push(@$queueRef, \@MODEL_SN) if (InternalVal($name, 'Seriennummer', '') eq ''); + + sendNextRequest($hash) if ($orgQueueCount == 0); + + return; +} + +sub DoChange(){ + my ($hash,$writeRef,$readRef) = @_; + my $name = $hash->{NAME}; + my $queueRef = $hash->{helper}{commandQueue}; + my $orgQueueCount = @$queueRef; + + push(@$queueRef, $writeRef); + push(@$queueRef, $readRef); + + sendNextRequest($hash) if ($orgQueueCount == 0); + + return; +} + +sub InitCommands() { + my ($hash) = @_; + my %commands; + + # map commands to actions + $commands{getCommandKey(@OUTDOOR_TEMPERATURE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Aussenluft_Temperatur", getTemperatur($subHash, $buf), 1); + }; + $commands{getCommandKey(@ROOM_TEMPERATURE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Raumluft_Temperatur_AirDail", getTemperatur($subHash, $buf), 1); + }; + $commands{getCommandKey(@SUPPLY_TEMPERATURE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Zuluft_Temperatur", getTemperatur($subHash, $buf), 1); + }; + $commands{getCommandKey(@EXTRACT_TEMPERATURE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Abluft_Temperatur", getTemperatur($subHash, $buf), 1); + }; + $commands{getCommandKey(@EXHAUST_TEMPERATURE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Fortluft_Temperatur", getTemperatur($subHash, $buf), 1); + }; + $commands{getCommandKey(@HUMIDITY)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Luftfeuchtigkeit", getHumidity($subHash, $buf), 1); + }; + $commands{getCommandKey(@AIR_INPUT)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Zuluft_Grundstufe_Einstellung", getAirInputOutput($subHash, $buf), 1); + }; + $commands{getCommandKey(@AIR_OUTPUT)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Abluft_Grundstufe_Einstellung", getAirInputOutput($subHash, $buf), 1); + }; + $commands{getCommandKey(@FAN_SPEED_SUPPLY)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Zuluft_Stufe", getAirInputOutput($subHash, $buf), 1); + }; + $commands{getCommandKey(@FAN_SPPED_EXTRACT)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Abluft_Stufe", getAirInputOutput($subHash, $buf), 1); + }; + $commands{getCommandKey(@FILTER_LIFE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "verbl.Filterlebensdauer", getFilterLifeTime($subHash, $buf), 1); + }; + $commands{getCommandKey(@BATTERY_LIFE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "verbl.Batterielebensdauer_AirDial", getBatteryLifeTime($subHash, $buf), 1); + }; + $commands{getCommandKey(@BOOST)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Stosslueftung_aktiviert", getONOFF($subHash, $buf), 1); + }; + $commands{getCommandKey(@BYPASS)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Bypass_aktiviert", getONOFF($subHash, $buf), 1); + }; + $commands{getCommandKey(@NIGHTCOOLING)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Nachtkuehlung_aktiviert", getONOFF($subHash, $buf), 1); + }; + $commands{getCommandKey(@FIREPLACE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Feuerstaette_aktiviert", getONOFF($subHash, $buf), 1); + }; + $commands{getCommandKey(@BOOST_AUTOMATIC)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "automatische_Stosslueftung", getOFFON($subHash, $buf), 1); + }; + $commands{getCommandKey(@BYPASS_AUTOMATIC)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "automatischer_Bypass", getOFFON($subHash, $buf), 1); + }; + $commands{getCommandKey(@FANSPEED_IN_RPM)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Zuluft_Luefterdrehzahl", getFanSpeedInRPM($subHash, $buf), 1); + }; + $commands{getCommandKey(@FANSPEED_OUT_RPM)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Abluft_Luefterdrehzahl", getFanSpeedInRPM($subHash, $buf), 1); + }; + $commands{getCommandKey(@MODE)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Modus", getMode($subHash, $buf), 1); + }; + $commands{getCommandKey(@FAN_STEP)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Luefterstufe_manuell", getFanSpeed($subHash, $buf), 1); + }; + $commands{getCommandKey(@BOOST_DURATION)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Stosslueftung_Dauer", getDurationTime($subHash, $buf), 1); + }; + $commands{getCommandKey(@BYPASS_DURATION)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Bypass_Dauer", getDurationTime($subHash, $buf), 1); + }; + $commands{getCommandKey(@OPERATION_TIME)} = sub { + my ($subHash,$buf) = @_; + readingsSingleUpdate( $subHash, "Arbeitsstunden", getOperationTime($subHash, $buf), 1); + }; + + # Internals + $commands{getCommandKey(@MODEL)} = sub { + my ($subHash,$buf) = @_; + $subHash->{Model} = getModel($subHash, $buf); + }; + $commands{getCommandKey(@MODEL_SN)} = sub { + my ($subHash,$buf) = @_; + $subHash->{Seriennummer} = getModelSN($subHash, $buf); + }; + + $hash->{helper}{commandHash} = \%commands; + + return; +} + +######################################## +############# GET Methoden ############# +######################################## + +sub getTemperatur() { + # read Temperaturen in Grad + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,2)); + Log3($name, 5, "AirUnit $name: recvunpackData in getTemperatur(): $tempresponse\n"); + + # handle negative values + my $temperature = unpack('s', pack('S', hex($tempresponse))); + # handle invalid value + return 'NaN' if ($temperature == -32768); + + return sprintf ('%.02f', $temperature/100); +} + +sub getHumidity() { + # read Luftfeuchtigkeit in Prozent + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getHumidity(): $tempresponse\n"); + my $humidity = hex($tempresponse) * 100 / 255; + return sprintf ('%.02f', $humidity); +} + +sub getAirInputOutput() { + # read in AIR_INPUT / AIR_OUTPUT und FAN_SPEED_SUPPLY / FAN_SPPED_EXTRACT in Prozent + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getAirInputOutput(): $tempresponse\n"); + my $inputoutput = hex($tempresponse); + return $inputoutput; +} + +sub getBatteryLifeTime() { + # read verbleibende Lebensdauer der Batterien im AirDail-Controller + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getBatteryLifeTime(): $tempresponse\n"); + my $batterylifetime = hex($tempresponse); + return sprintf ('%.02f', $batterylifetime); +} + +sub getFilterLifeTime() { + # read verbleibende Lebensdauer der Filter in Prozent + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getFilterLifeTime(): $tempresponse\n"); + my $filterlifetime = hex($tempresponse) * 100 / 255; + return sprintf ('%.02f', $filterlifetime); +} + +sub getONOFF() { + # read true/1/ON or false/0/OFF for BOOST, BYPASS, NIGHTCOOLING, FIREPLACE + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $onoff = hex(unpack("H*" , substr($data,0,1))); + Log3($name, 5, "AirUnit $name: recvunpackData in getONOFF(): $onoff\n"); + if($onoff == 1){ + return "An" + }elsif($onoff == 0){ + return "Aus" + }elsif($onoff == 255){ #für aktueller Status des Bypasses (aktiv) + return "An" + }else { + Log3($name, 1, "AirUnit $name: Unbekannter Paramter in getONOFF(): $onoff\n"); + } + + return; +} + +sub getOFFON() { + # read true/1/OFF or false/0/ON for DISABLE_BOOST_AUTOMATIC, DISABLE_BOOST_AUTOMATIC + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $offon = hex(unpack("H*" , substr($data,0,1))); + Log3($name, 5, "AirUnit $name: recvunpackData in getOFFON(): $offon\n"); + if($offon == 0){ + return "An" + }elsif($offon == 1){ + return "Aus" + }else { + Log3($name, 1, "AirUnit $name: Unbekannter Paramter in getOFFON(): $offon\n"); + } + + return; +} + +sub getFanSpeedInRPM() { + # read aktuelle Lüftergeschwindigkeit in U/min + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,2)); + Log3($name, 5, "AirUnit $name: recvunpackData in getFanSpeedInRPM(): $tempresponse\n"); + my $fanspeedinrpm = hex($tempresponse); + return $fanspeedinrpm; +} + +sub getFanSpeed() { + # read aktuelle Lüftergeschwindigkeit in Stufen von 1-10 + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getFanspeed(): $tempresponse\n"); + my $fanspeed = hex($tempresponse); + return $fanspeed; +} + +sub getModel() { + # read Modeltyp + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $model = unpack("A*" , substr($data,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getModel(): $model\n"); + return $model; +} + +sub getMode() { + # read Anlagenmodus + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getMode(): $tempresponse\n"); + my $getmode = hex($tempresponse); + if($getmode == 0){ + return "Bedarfsmodus" + }elsif($getmode == 1){ + return "Programm" + }elsif($getmode == 2){ + return "Manuell" + }elsif($getmode == 3){ + return "Aus" + }else { + Log3($name, 1, "AirUnit $name: Unbekannter Antwortparamter in getMode(): $getmode\n"); + } + return; +} + +sub getModelSN() { + # read Seriennummer + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,2)); + Log3($name, 5, "AirUnit $name: recvunpackData in getModelSN(): $tempresponse\n"); + my $modelsn = hex($tempresponse); + return $modelsn; +} + +sub getDurationTime() { + # read aktuelle Dauer in Stunden für BYPASS_DURATION and BOOST_DURATION + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*" , substr($data,0,1)); + Log3($name, 5, "AirUnit $name: recvunpackData in getDurationTime(): $tempresponse\n"); + my $getduration = hex($tempresponse); + return $getduration; +} + +sub getOperationTime() { + # read Arbeitsstunden + my ($hash,$data) = @_; + my $name = $hash->{NAME}; + + my $tempresponse = unpack("H*", substr($data,0,4)); + Log3($name, 5, "AirUnit $name: recvunpackData in getOperationTime(): $tempresponse\n"); + my $operationtime = hex($tempresponse) / 60; + return sprintf ('%.01f', $operationtime); +} + +sub getCommandKey() { + my (@command) = @_; + return unpack('H*', pack('C*' x @command, @command)); +} + +####################### SEND Request ####################### +# VERBINDUNGSAUFBAU ZUR ANLAGE... +############################################################ + +# SEND Request +sub sendNextRequest(){ + my ($hash,$error) = @_; + my $name = $hash->{NAME}; + my $queueRef = $hash->{helper}{commandQueue}; + + # create a log entry with the error message + if ($error) { + Log3($name, 5, "AirUnit $name: error while connecting: $error"); + return; + } + + # queue is empty / device disabled - nothing to do + if (!@$queueRef || AttrVal($name, "disable", 0) == 1) { + $queueRef = []; + ::DevIo_CloseDev($hash); + return; + } + + # open connection to send commands + if (!::DevIo_IsOpen($hash)) { + my $reopen = Value($name) eq 'opened' ? 1 : 0; + ::DevIo_OpenDev( $hash, $reopen, undef, \&sendNextRequest); + return; + } + + my @nextCmd = @{ shift(@$queueRef) }; + my $data = pack('C*' x @nextCmd, @nextCmd); + my $unpackedData = unpack('H*', $data); + + Log3($name, 4, "AirUnit $name: sendData in sendRequest(): $unpackedData"); + ::DevIo_SimpleWrite( $hash, $data, 0 ); + + $hash->{LastCommand} = $unpackedData; + $hash->{LastCommandTS} = TimeNow(); + + return; +} + +1; + +=pod +=item summary controls Danfoss AirUnits (a1,a2,w1,w2) +=begin html + +<a name="AirUnit"></a> +<h3>AirUnit</h3> +<ul> + <i>AirUnit</i> implements a FHEM device to control Danfoss AirUnits (a1,a2,w1,w2). Tested only with w2 (Feb 2021). + With this module it is possible to control the most useful functions of your ventilation system. + <br><br> + + <table> + <tr> + <th>possible Readings</th> + <th>units of values</th> + </tr> + <tr> + <td>Abluft_Grundstufe_Einstellung</td> + <td>percent</td> + </tr> + <tr> + <td>Abluft_Luefterdrehzahl</td> + <td>rpm</td> + </tr> + <tr> + <td>Abluft_Stufe</td> + <td>step</td> + </tr> + <tr> + <td>Abluft_Temperatur</td> + <td>degree</td> + </tr> + <tr> + <td>Aussenluft_Temperatur</td> + <td>degree</td> + </tr> + <tr> + <td>Bypass_aktiviert</td> + <td>on/off</td> + </tr> + <tr> + <td>Bypass_Dauer</td> + <td>hour</td> + </tr> + <tr> + <td>Feuerstaette_aktiviert</td> + <td>on/off</td> + </tr> + <tr> + <td>Fortluft_Temperatur</td> + <td>degree</td> + </tr> + <tr> + <td>Luefterstufe_manuell</td> + <td>step</td> + </tr> + <tr> + <td>Luftfeuchtigkeit</td> + <td>percent</td> + </tr> + <tr> + <td>Model</td> + <td>name</td> + </tr> + <tr> + <td>Modus</td> + <td>mode of operation</td> + </tr> + <tr> + <td>Nachtkuehlung_aktiviert</td> + <td>on/off</td> + </tr> + <tr> + <td>Raumluft_Temperatur_AirDail</td> + <td>degree</td> + </tr> + <tr> + <td>Seriennummer</td> + <td>number</td> + </tr> + <tr> + <td>Stosslueftung_aktiviert</td> + <td>on/off</td> + </tr> + <tr> + <td>Stosslueftung_Dauer</td> + <td>hour</td> + </tr> + <tr> + <td>Zuluft_Grundstufe_Einstellung</td> + <td>percent</td> + </tr> + <tr> + <td>Zuluft_Luefterdrehzahl</td> + <td>rpm</td> + </tr> + <tr> + <td>Zuluft_Stufe</td> + <td>step</td> + </tr> + <tr> + <td>Zuluft_Temperatur</td> + <td>degree</td> + </tr> + <tr> + <td>automatische_Stosslueftung</td> + <td>on/off</td> + </tr> + <tr> + <td>automatischer_Bypass</td> + <td>on/off</td> + </tr> + <tr> + <td>verbl.Batterielebensdauer_AirDial</td> + <td>percent</td> + </tr> + <tr> + <td>verbl.verbl.Filterlebensdaue</td> + <td>percent</td> + </tr> + <tr> + <td>Arbeitsstunden</td> + <td>hour</td> + </tr> + </table> + <br><br> + + + <a name="AirUnitdefine"></a> + <b>Define</b> + <ul> + <code>define <name> AirUnit <IP-address[:Port]> [poll-interval]</code><br> + If the poll interval is omitted, it is set to 300 (seconds). Smallest possible value is 30. + <br> + Usually, the port needs not to be defined. + <br> + Example: <code>define myAirUnit AirUnit 192.168.0.12 600</code> + </ul> + <br> + + <a name="AirUnitset"></a> + <b>Set</b><br> + <ul> + <code>set <name> <option> <value></code> + <br><br> + You can <i>set</i> different values to any of the following options. + <br><br> + Options: + <ul> + <li><i>Modus</i><br> + You can choose between<br> + <i>"Bedarfsmodus"</i>, for automatic mode<br> + <i>"Programm"</i>, you can define a programm in your AirDail-Controller and choose one.<br> + <i>"Manuell"</i>, you can set the steps for the fans manually (only in manual mode). Bypass and Boost are in automatic mode.<br> + <i>"Aus"</i>, the system is off for 24 hours, after this time, the system starts in automatic mode with fanstep 1.</li> + <li><i>Luefterstufe</i><br> + You can set the steps for the fans manually. (only in manual mode)</li> + <li><i>Stosslueftung</i><br> + You can activate/deactive the Boost-Option of your ventilation system. You can configure this mode in your AirDail-Controller, the standard fanstep 10 for 3 hours.<br> + It is useful if you need more Air e.g. in case of cooking or a party with more people.</li> + <li><i>Stosslueftung_Dauer</i><br> + You can set the hours for the duration of Boost-Option manually.</li> + <li><i>Bypass</i><br> + You can activate/deactive the Bypass-Option of you ventilations systems. Its a cooling function, the heat exchanger will be deactivated.<br> + You can configure this mode in your AirDail-Controller, the standard time is 3 hours.<br> + <b>You can't activte it, if the outdoor temperature is under 5°C.<br> + This option is not available for w1-unit.</b></li> + <li><i>Bypass_Dauer</i><br> + You can set the hours for the duration of Bypass-Option manually.</li> + <li><i>Nachtkuehlung</i><br> + You can activate/deactive the nightcooling option of you ventilations systems. You can configure this in your AirDail-Controller.<br> + <b>This option is not available for w1-unit.</b></li> + <li><i>automatische_Stosslueftung</i><br> + You can activate/deactive the automatic Boost-Option of you ventilations systems. Its automaticly activated, if the humidity increase very strong, then it runs for 30min.</li> + <li><i>automatischer_Bypass</i><br> + You can activate/deactive the automatic Bypass-Option of you ventilations systems. Its automaticly activated, if the outdoor temperature and room temperature are higher then the configured values.<br> + You can configure this mode in your AirDail-Controller.</li> + <li><i>Intervall</i><br> + You can setup the refresh intervall of your readings. Minimum 30 seconds.</li> + <li><i>Feuerstaette</i><br> + You can activate/deactive the Fireplace-Option.</li> + </ul> + </ul> + <br> + + <a name="AirUnitget"></a> + <b>Get</b><br> + <ul> + <code>set <name> <option> <value></code> + <br><br> + You can <i>set</i> different values to any of the following options. + <br><br> + Options: + <ul> + <li><i>update</i><br> + You can refresh all values manually.</li> + </ul> + </ul> + <br> + + <a name="AirUnitattr"></a> + <b>Attributes</b> + <ul> + <code>attr <name> <attribute> <value></code> + <br><br> + Attributes: + <ul> + <li><i>disable</i> 0|1<br> + When you set disable to "1", the connection and the refresh intervall will be disabled. (takes a while)<br> + This feature gives you the possibility to use an external connection (e.g. the Danfoss-Windows-Tool) without deletion of the device.<br> + </li> + </ul> + </ul> + <br> + + <a name="AirUnitaddinfo"></a> + <b>additional information</b> + <ul> + <li><i>PC-Tool</i><br> + You can donwload the Danfoss-Windows-Tool <a href="https://www.danfoss.com/da-dk/service-and-support/downloads/dhs/danfoss-air-pc-tool-end-user/#tab-overview">HERE</a>.<br> + You can start this tool with 3 different options: enduser, service or installer.<br> + You need only to change the "HRVPCTool.exe.config" in the installation directory.<br> + <ul> + <li>enduser {47464213-F94A-495e-81A0-486E54CB4F64}</li> + <li>service {FC0CB02C-1695-4064-BCD9-FC0A5D77ED3D}</li> + <li>installer {E4C3938B-9F3E-427e-85CF-A42FE350326D}</li> + </ul> + </li> + </ul> + +</ul> + +=end html + +=cut \ No newline at end of file