# $Id: 98_AirUnit.pm 27775 2023-07-16 17:51:09Z vuffiraa $
##############################################################################
#
#     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 &lt;name&gt; AirUnit &lt;IP-address[:Port]&gt; [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 &lt;name&gt; &lt;option&gt; &lt;value&gt;</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 &lt;name&gt; &lt;option&gt; &lt;value&gt;</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 &lt;name&gt; &lt;attribute&gt; &lt;value&gt;</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