From cf318d71ebfd6c39eec95f8e82265cf9f6c8b167 Mon Sep 17 00:00:00 2001 From: Wzut <> Date: Tue, 28 Apr 2020 16:47:07 +0000 Subject: [PATCH] 10_MAX.pm: Beta version git-svn-id: https://svn.fhem.de/fhem/trunk@21795 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/contrib/Wzut/.gitkeep | 0 fhem/contrib/Wzut/10_MAX.pm | 2589 +++++++++++++++++++++++++++++++++++ 2 files changed, 2589 insertions(+) delete mode 100644 fhem/contrib/Wzut/.gitkeep create mode 100644 fhem/contrib/Wzut/10_MAX.pm diff --git a/fhem/contrib/Wzut/.gitkeep b/fhem/contrib/Wzut/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/fhem/contrib/Wzut/10_MAX.pm b/fhem/contrib/Wzut/10_MAX.pm new file mode 100644 index 000000000..fb4b72fb9 --- /dev/null +++ b/fhem/contrib/Wzut/10_MAX.pm @@ -0,0 +1,2589 @@ +# $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