# $Id: $ # # (c) 2019 Copyright: Wzut # (c) 2012 Copyright: Matthias Gehre, M.Gehre@gmx.de # # All rights reserved # # FHEM Forum : https://forum.fhem.de/index.php/board,23.0.html # # This code 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. # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # This script 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. # # 2.0.0 => 28.03.2020 # 1.0.0" => (c) M.Gehre ################################################################ package main; use strict; use warnings; use AttrTemplate; use MIME::Base64; use Date::Parse; my %device_types = ( 0 => "Cube", 1 => "HeatingThermostat", 2 => "HeatingThermostatPlus", 3 => "WallMountedThermostat", 4 => "ShutterContact", 5 => "PushButton", 6 => "virtualShutterContact", 7 => "virtualThermostat", 8 => "PlugAdapter", 9 => "new" ); my %msgId2Cmd = ( "00" => "PairPing", "01" => "PairPong", "02" => "Ack", "03" => "TimeInformation", "10" => "ConfigWeekProfile", "11" => "ConfigTemperatures", #like eco/comfort etc "12" => "ConfigValve", "20" => "AddLinkPartner", "21" => "RemoveLinkPartner", "22" => "SetGroupId", "23" => "RemoveGroupId", "30" => "ShutterContactState", "40" => "SetTemperature", # to thermostat "42" => "WallThermostatControl", # by WallMountedThermostat # Sending this without payload to thermostat sets desiredTempeerature to the comfort/eco temperature # We don't use it, we just do SetTemperature "43" => "SetComfortTemperature", "44" => "SetEcoTemperature", "50" => "PushButtonState", "60" => "ThermostatState", # by HeatingThermostat "70" => "WallThermostatState", "82" => "SetDisplayActualTemperature", "F1" => "WakeUp", "F0" => "Reset", ); my %msgCmd2Id = reverse %msgId2Cmd; my $defaultWeekProfile = "444855084520452045204520452045204520452045204520452044485508452045204520452045204520452045204520452045204448546c44cc55144520452045204520452045204520452045204448546c44cc55144520452045204520452045204520452045204448546c44cc55144520452045204520452045204520452045204448546c44cc55144520452045204520452045204520452045204448546c44cc5514452045204520452045204520452045204520"; my @ctrl_modes = ( "auto", "manual", "temporary", "boost" ); my %boost_durations = (0 => 0, 1 => 5, 2 => 10, 3 => 15, 4 => 20, 5 => 25, 6 => 30, 7 => 60); my %boost_durationsInv = reverse %boost_durations; my %decalcDays = (0 => "Sat", 1 => "Sun", 2 => "Mon", 3 => "Tue", 4 => "Wed", 5 => "Thu", 6 => "Fri"); my @weekDays = ("Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"); my %decalcDaysInv = reverse %decalcDays; my %readingDef = ( #min/max/default "maximumTemperature" => [ \&MAX_validTemperature, "on"], "minimumTemperature" => [ \&MAX_validTemperature, "off"], "comfortTemperature" => [ \&MAX_validTemperature, 21], "ecoTemperature" => [ \&MAX_validTemperature, 17], "windowOpenTemperature" => [ \&MAX_validTemperature, 12], "windowOpenDuration" => [ \&MAX_validWindowOpenDuration, 15], "measurementOffset" => [ \&MAX_validMeasurementOffset, 0], "boostDuration" => [ \&MAX_validBoostDuration, 5 ], "boostValveposition" => [ \&MAX_validValveposition, 80 ], "decalcification" => [ \&MAX_validDecalcification, "Sat 12:00" ], "maxValveSetting" => [ \&MAX_validValveposition, 100 ], "valveOffset" => [ \&MAX_validValveposition, 00 ], "groupid" => [ \&MAX_validGroupid, 0 ], ".weekProfile" => [ \&MAX_validWeekProfile, $defaultWeekProfile ] ); #my %interfaces = ( # "Cube" => undef, # "HeatingThermostat" => "thermostat;battery;temperature", # "HeatingThermostatPlus" => "thermostat;battery;temperature", # "WallMountedThermostat" => "thermostat;temperature;battery", # "ShutterContact" => "switch_active;battery", # "PushButton" => "switch_passive;battery" # ); sub MAX_validTemperature { return $_[0] eq "on" || $_[0] eq "off" || ($_[0] =~ /^\d+(\.[05])?$/ && $_[0] >= 4.5 && $_[0] <= 30.5); } # Identify for numeric values and maps "on" and "off" to their temperatures sub MAX_ParseTemperature { return $_[0] eq "on" ? 30.5 : ($_[0] eq "off" ? 4.5 :$_[0]); } sub MAX_validWindowOpenDuration { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 60; } sub MAX_validMeasurementOffset { return $_[0] =~ /^-?\d+(\.[05])?$/ && $_[0] >= -3.5 && $_[0] <= 3.5; } sub MAX_validBoostDuration { return $_[0] =~ /^\d+$/ && exists($boost_durationsInv{$_[0]}); } sub MAX_validValveposition { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 100; } sub MAX_validWeekProfile { return length($_[0]) == 4*13*7; } sub MAX_validGroupid { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 255; } sub MAX_validDecalcification { my ($decalcDay, $decalcHour) = ($_[0] =~ /^(...) (\d{1,2}):00$/); return defined($decalcDay) && defined($decalcHour) && exists($decalcDaysInv{$decalcDay}) && 0 <= $decalcHour && $decalcHour < 24; } sub MAX_Initialize { my ($hash) = shift; $hash->{Match} = "^MAX"; $hash->{DefFn} = "MAX_Define"; $hash->{UndefFn} = "MAX_Undef"; $hash->{ParseFn} = "MAX_Parse"; $hash->{SetFn} = "MAX_Set"; $hash->{GetFn} = "MAX_Get"; $hash->{RenameFn} = "MAX_RenameFn"; $hash->{NotifyFn} = "MAX_Notify"; $hash->{DbLog_splitFn} = "MAX_DbLog_splitFn"; $hash->{AttrFn} = "MAX_Attr"; $hash->{AttrList} = "IODev CULdev actCycle do_not_notify:1,0 ignore:0,1 dummy:0,1 keepAuto:0,1 debug:0,1 scanTemp:0,1 skipDouble:0,1 externalSensor ". "model:HeatingThermostat,HeatingThermostatPlus,WallMountedThermostat,ShutterContact,PushButton,Cube,PlugAdapter autosaveConfig:1,0 ". "peers sendMode:peers,group,Broadcast dTempCheck:0,1 windowOpenCheck:0,1 DbLog_log_onoff:0,1 ".$readingFnAttributes; return; } ############################# sub MAX_Define { my $hash = shift; my $def = shift; my @arg = split("[ \t][ \t]*", $def); my $name = $hash->{NAME}; return 'name '.$name.' is reserved for internal use' if($name eq 'fakeWallThermostat' or $name eq 'fakeShutterContact'); return 'wrong syntax: define MAX type address' if(int(@arg)!=4 || $arg[3] !~ m/^[a-fA-F0-9]{6}$/i); return 'incorrect address 000000' if ($arg[3] eq '000000'); my $type = $arg[2]; my $addr = lc($arg[3]); #all addr should be lowercase if(exists($modules{MAX}{defptr}{$addr}) && $modules{MAX}{defptr}{$addr}->{NAME} ne $name) { my $msg = 'MAX_Define, a Device with addr '.$addr.' is already defined ('.$modules{MAX}{defptr}{$addr}->{NAME}.')'; Log3 $hash, 2, $msg; return $msg; } return $type." is not a valid MAX type !" if (!MAX_TypeToTypeId($type) && ($type ne 'Cube')); Log3 $hash, 5, 'Max_define, '.$name.' '.$type.' with addr '.$addr; $hash->{type} = $type; $hash->{devtype} = MAX_TypeToTypeId($type); $hash->{addr} = $addr; $hash->{TimeSlot} = -1 if ($type =~ /.*Thermostat.*/); # wird durch CUL_MAX neu gesetzt $hash->{'.count'} = 0; # ToDo Kommentar $hash->{'.sendToAddr'} = '-1'; # zu wem haben wird direkt gesendet ? $hash->{'.sendToName'} = ''; $hash->{'.timer'} = 300; $modules{MAX}{defptr}{$addr} = $hash; #$hash->{internals}{interfaces} = $interfaces{$type}; # wozu ? AssignIoPort($hash); CommandAttr(undef,$name.' model '.$type); # Forum Stats werten nur attr model aus if ($init_done == 1) { #nur beim ersten define setzen: if ((($hash->{devtype} > 0) && ($hash->{devtype} < 4)) || ($hash->{devtype} == 7)) { $attr{$name}{room} = "MAX" if( not defined( $attr{$name}{room} ) ); MAX_ReadingsVal($hash,'groupid'); MAX_ReadingsVal($hash,'windowOpenTemperature') if ($hash->{devtype} == 7); readingsBeginUpdate($hash); MAX_ParseWeekProfile($hash); readingsEndUpdate($hash,0); } } RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5, "MAX_Timer", $hash, 0) if ($hash->{devtype} != 5); return; } sub MAX_Timer { my $hash = shift; my $name = $hash->{NAME}; if (!$init_done) { InternalTimer(gettimeofday()+5,"MAX_Timer", $hash, 0); return; } InternalTimer(gettimeofday() + $hash->{'.timer'}, "MAX_Timer", $hash, 0) if ($hash->{'.timer'}); return if (IsDummy($name) || IsIgnored($name)); if ($hash->{devtype} && (($hash->{devtype} < 4) || ($hash->{devtype} == 8))) { my $dt = ReadingsNum($name,'desiredTemperature',0); if ($dt == ReadingsNum($name,'windowOpenTemperature','0')) # kein check bei offenen Fenster { my $age = sprintf "%02d:%02d", (gmtime(ReadingsAge($name,'desiredTemperature', 0)))[2,1]; readingsSingleUpdate($hash,'windowOpen', $age,1) if (AttrNum($name,'windowOpenCheck',0)); $hash->{'.timer'} = 60; return; } if ((ReadingsVal($name,'mode','manu') eq 'auto') && AttrNum($name,'dTempCheck',0)) { $hash->{saveConfig} = 1; # verhindern das alle weekprofile Readings neu geschrieben werden MAX_ParseWeekProfile($hash); # $hash->{helper}{dt} aktualisieren delete $hash->{saveConfig}; my $c = ($dt != $hash->{helper}{dt}) ? sprintf("%.1f", ($dt-$hash->{helper}{dt})) : 0; delete $hash->{helper}{dtc} if (!$c && exists($hash->{helper}{dtc})); if ($c && (!exists($hash->{helper}{dtc}))) {$hash->{helper}{dtc}=1; $c=0; }; # um eine Runde verzögern readingsBeginUpdate($hash); readingsBulkUpdate($hash,'dTempCheck', $c); readingsBulkUpdate($hash,'windowOpen', '0') if (AttrNum($name,'windowOpenCheck',0)); readingsEndUpdate($hash,1); $hash->{'.timer'} = 300; Log3 $hash,3,$name.', Tempcheck NOK Reading : '.$dt.' <-> WeekProfile : '.$hash->{helper}{dt} if ($c); } } elsif ((($hash->{devtype} == 4) || ($hash->{devtype} == 6)) && AttrNum($name,'windowOpenCheck',1)) { if (ReadingsNum($name,'onoff',0)) { my $age = (sprintf "%02d:%02d", (gmtime(ReadingsAge($name,'onoff', 0)))[2,1]); readingsSingleUpdate($hash,'windowOpen', $age ,1); $hash->{'.timer'} = 60; } else { readingsSingleUpdate($hash,'windowOpen', '0',1); $hash->{'.timer'} = 300; } } return; } sub MAX_Attr { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; if ($cmd eq 'del') { return 'FHEM statistics are using this, please do not delete or change !' if ($attrName eq 'model'); $hash->{'.actCycle'} = 0 if ($attrName eq 'actCycle'); if ($attrName eq 'externalSensor') { delete($hash->{NOTIFYDEV}); notifyRegexpChanged($hash,'global'); } return; } if ($cmd eq 'set') { if ($attrName eq 'model') { #$$attrVal = $hash->{type}; bzw. $_[3] = $hash->{type} , muss das sein ? return "$name, model is $hash->{type}" if ($attrVal ne $hash->{type}); } elsif ($attrName eq 'dummy') { $attr{$name}{scanTemp} = '0' if (AttrNum($name,'scanTemp',0) && int($attrVal)); } elsif ($attrName eq 'CULdev') { # ohne Abfrage von init_done : Reihenfoleproblem in der fhem.cfg ! return "$name, invalid CUL device $attrVal" if (!exists($defs{$attrVal}) && $init_done); } elsif ($attrName eq 'actCycle') { my @ar = split(':',$attrVal); $ar[0] = 0 if (!$ar[0]); $ar[1] = 0 if (!$ar[1]); my $v = (int($ar[0])*3600) + (int($ar[1])*60); $hash->{'.actCycle'} = $v if ($v >= 0); } elsif ($attrName eq 'externalSensor') { return $name.', attribute externalSensor is not supported for this device !' if ($hash->{devtype}>2) && ($hash->{devtype}<6); my ($sd,$sr,$sn) = split (':',$attrVal); if($sd && $sr && $sn) { notifyRegexpChanged($hash,'$sd:$sr'); $hash->{NOTIFYDEV}=$sd; } } } return; } sub MAX_Undef { my $hash = shift; delete($modules{MAX}{defptr}{$hash->{addr}}); return; } sub MAX_TypeToTypeId { my $id = shift; foreach (keys %device_types) { return $_ if($id eq $device_types{$_}); } return 0; } sub MAX_CheckIODev { my $hash = shift; return !defined($hash->{IODev}) || ($hash->{IODev}{TYPE} ne "MAXLAN" && $hash->{IODev}{TYPE} ne "CUL_MAX"); } # Print number in format "0.0", pass "on" and "off" verbatim, convert 30.5 and 4.5 to "on" and "off" # Used for "desiredTemperature", "ecoTemperature" etc. but not "temperature" #sub MAX_SerializeTemperature($) #{ #if (($_[0] eq "on") || ($_[0] eq "off")) { return $_[0]; } #elsif($_[0] == 4.5) { return "off"; } #elsif($_[0] == 30.5) { return "on"; } #return sprintf("%2.1f",$_[0]); #} sub MAX_SerializeTemperature { my $t = shift; return $t if ( $t =~ /^(on|off)$/ ); return 'off' if ( $t == 4.5 ); return 'on' if ( $t == 30.5 ); return sprintf("%2.1f", $t); } sub MAX_Validate # Todo : kann das weg ? { my $name = shift; my $val = shift; return 1 if (!exists($readingDef{$name})); return $readingDef{$name}[0]->($val); } # Get a reading, validating it's current value (maybe forcing to the default if invalid) # "on" and "off" are converted to their numeric values sub MAX_ReadingsVal { my $hash = shift; my $reading = shift; my $newval = shift; my $name = $hash->{NAME}; if (defined($newval)) { return if ($newval eq ''); if (exists($hash->{".updateTimestamp"})) # readingsBulkUpdate ist aktiv, wird von fhem.pl gesetzt/gelöscht { readingsBulkUpdate($hash,$reading,$newval); } else { readingsSingleUpdate($hash,$reading,$newval,1); } return; } my $val = ReadingsVal($name,$reading,""); # $readingDef{$name} array is [validatingFunc, defaultValue] if (exists($readingDef{$reading}) && (!$readingDef{$reading}[0]->($val))) { #Error: invalid value my $err = "invalid or missing value $val for READING $reading"; $val = $readingDef{$reading}[1]; Log3 $hash, 3, "$name, $err , forcing to $val"; #Save default value to READINGS if (exists($hash->{".updateTimestamp"})) # readingsBulkUpdate ist aktiv, wird von fhem.pl gesetzt/gelöscht { readingsBulkUpdate($hash,$reading,$val); readingsBulkUpdate($hash,'error',$err); } else { readingsBeginUpdate($hash); readingsBulkUpdate($hash,$reading,$val); readingsBulkUpdate($hash,'error',$err); readingsEndUpdate($hash,0); } } return MAX_ParseTemperature($val); # ToDo : nochmal alle Aufrufe duchsehen ob das hier Sinn macht } sub MAX_ParseWeekProfile { my $hash = shift; my @lines = undef; # Format of weekprofile: 16 bit integer (high byte first) for every control point, 13 control points for every day # each 16 bit integer value is parsed as # int time = (value & 0x1FF) * 5; # int hour = (time / 60) % 24; # int minute = time % 60; # int temperature = ((value >> 9) & 0x3F) / 2; my $curWeekProfile = MAX_ReadingsVal($hash, ".weekProfile"); my (undef,$min,$hour,undef,undef,undef,$wday) = localtime(gettimeofday()); # (Sun,Mon,Tue,Wed,Thu,Fri,Sat) -> localtime # (Sat,Sun,Mon,Tue,Wed,Thu,Fri) -> MAX intern $wday++; # localtime = MAX Day; $wday -= 7 if ($wday > 6); my $daymins = ($hour*60)+$min; $hash->{helper}{dt} = -1; #parse weekprofiles for each day for (my $i=0;$i<7;$i++) { $hash->{helper}{myday} = $i if ($i == $wday); my (@time_prof, @temp_prof); for(my $j=0;$j<13;$j++) { $time_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))& 0x1FF) * 5; $temp_prof[$j] = (hex(substr($curWeekProfile,($i*52)+ 4*$j,4))>> 9 & 0x3F ) / 2; } my @hours; my @minutes; my $j; # ToDo umschreiben ! for($j=0;$j<13;$j++) { $hours[$j] = ($time_prof[$j] / 60 % 24); $minutes[$j] = ($time_prof[$j]%60); #if 00:00 reached, last point in profile was found if (int($hours[$j]) == 0 && int($minutes[$j]) == 0) { $hours[$j] = 24; last; } } my $time_prof_str = "00:00"; my $temp_prof_str; my $line =''; my $json_ti =''; my $json_te =''; for (my $k=0;$k<=$j;$k++) { $time_prof_str .= sprintf("-%02d:%02d", $hours[$k], $minutes[$k]); $temp_prof_str .= sprintf("%2.1f °C",$temp_prof[$k]); my $t = (sprintf("%2.1f",$temp_prof[$k])+0); $line .= $t.','; $json_te .="\"$t\""; $t = sprintf("%02d:%02d", $hours[$k], $minutes[$k]); $line .= $t; $json_ti .="\"$t\""; if (($i == $wday) && (((($hours[$k]*60)+$minutes[$k]) > $daymins) && ($hash->{helper}{dt} < 0))) { # der erste Schaltpunkt in der Zukunft ist es $hash->{helper}{dt} = sprintf("%.1f",$temp_prof[$k]); } if ($k < $j) { $time_prof_str .= " / " . sprintf("%02d:%02d", $hours[$k], $minutes[$k]); $temp_prof_str .= " / "; $line .= ','; $json_ti .=','; $json_te .=','; } } if (!defined($hash->{saveConfig})) { readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-time", $time_prof_str ); readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-temp", $temp_prof_str ); } else { push @lines ,'set '.$hash->{NAME}.' weekProfile '.$decalcDays{$i}.' '.$line; push @lines ,'setreading '.$hash->{NAME}." weekprofile-$i-$decalcDays{$i}-time ".$time_prof_str; push @lines ,'setreading '.$hash->{NAME}." weekprofile-$i-$decalcDays{$i}-temp ".$temp_prof_str; push @lines ,'"'.$decalcDays{$i}.'":{"time":['.$json_ti.'],"temp":['.$json_te.']}'; } } return @lines; } ############################# sub MAX_WakeUp { my $hash = shift; #3F corresponds to 31 seconds wakeup (so its probably the lower 5 bits) return ($hash->{IODev}{Send})->($hash->{IODev},"WakeUp",$hash->{addr}, "3F", callbackParam => "31" ); } sub MAX_Get { my $hash = shift; my $name = shift; my $cmd = shift; return "no get value specified" if(!$cmd); my $dev = shift; return if (IsDummy($name) || IsIgnored($name) || ($hash->{devtype} == 6)); my $backuped_devs = MAX_BackupedDevs($name); return if(!$backuped_devs); if ($cmd eq 'show_savedConfig') { my $ret; my $dir = AttrVal('global','logdir','./log/'); $dir .='/' if ($dir !~ m/\/$/); my ($error,@lines) = FileRead($dir.$dev.'.max'); return $error if($error); foreach (@lines) { $ret .= $_."\n"; } return $ret; } return 'unknown argument '.$cmd.' , choose one of show_savedConfig:'.$backuped_devs; } sub MAX_Set($@) { my ($hash, $devname, @ar) = @_; my ($setting, @args) = @ar; my $ret = ''; my $devtype = int($hash->{devtype}); return if (IsDummy($devname) || IsIgnored($devname) || !$devtype || ($setting eq 'valveposition')); if ($setting eq 'mode') { if ($args[0] eq 'auto') { $setting='desiredTemperature';} if ($args[0] eq 'manual') { $setting ='desiredTemperature'; $args[0] = ReadingsVal($devname,'desiredTemperature','20') if (!$args[1]); } } if (($setting eq "export_Weekprofile") && ReadingsVal($devname,'.wp_json','')) { return CommandSet(undef, $args[0].' profile_data '.$devname.' '.ReadingsVal($devname,'.wp_json','')); } elsif ($setting eq "saveConfig") { return MAX_saveConfig($devname,$args[0]); } elsif ($setting eq "saveAll") { return MAX_Save('all'); } elsif (($setting eq "restoreReadings") || ($setting eq "restoreDevice")) { my $f = $args[0]; $args[0] =~ s/(.)/sprintf("%x",ord($1))/eg; return if (!$f || ($args[0] eq 'c2a0')); return MAX_Restore($devname,$setting,$f); } elsif($setting eq "deviceRename") { my $newName = $args[0]; return CommandRename(undef,$devname.' '.$newName); } return $devname.', invalid IODev' if(MAX_CheckIODev($hash)); return $devname.', can not set without IODev' if(!exists($hash->{IODev})); if($setting eq 'desiredTemperature' and $hash->{type} =~ /.*Thermostat.*/) { return $devname.', missing value' if(!@args); my $temperature; my $until = undef; my $ctrlmode = 1; # 0=auto, 1=manual; 2=temporary if($args[0] eq "auto") { # This enables the automatic/schedule mode where the thermostat follows the weekly program # There can be a temperature supplied, which will be kept until the next switch point of the weekly program if(@args == 2) { if($args[1] eq "eco") { $temperature = MAX_ReadingsVal($hash,"ecoTemperature"); } elsif($args[1] eq "comfort") { $temperature = MAX_ReadingsVal($hash,"comfortTemperature"); } else { $temperature = MAX_ParseTemperature($args[1]); } } elsif(@args == 1) { $temperature = 0; # use temperature from weekly program } else { return $devname.', too many parameters: desiredTemperature auto []'; } $ctrlmode = 0; #auto } # auto elsif($args[0] eq "boost") { return $devname.', too many parameters: desiredTemperature boost' if(@args > 1); $temperature = 0; $ctrlmode = 3; #TODO: auto mode with temperature is also possible } else { if($args[0] eq "manual") { # User explicitly asked for manual mode $ctrlmode = 1; #manual, possibly overwriting keepAuto shift @args; return $devname.', not enough parameters after desiredTemperature manual' if(!@args); } elsif(AttrNum($devname,'keepAuto',0) && (MAX_ReadingsVal($hash,'mode') eq 'auto')) { # User did not ask for any mode explicitly, but has keepAuto Log3 $hash, 5, $devname.', Set: staying in auto mode'; $ctrlmode = 0; # auto } if($args[0] eq 'eco') { $temperature = MAX_ReadingsVal($hash,'ecoTemperature'); } elsif($args[0] eq 'comfort') { $temperature = MAX_ReadingsVal($hash,'comfortTemperature'); } else { $temperature = MAX_ParseTemperature($args[0]); } if(@args > 1) { # @args == 3 and $args[1] == "until" return $devname.', second parameter must be until' if($args[1] ne 'until'); return $devname.', wrong parameters : desiredTemperature until