# $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 Date::Parse; my %device_types = ( 0 => 'Cube', 1 => 'HeatingThermostat', 2 => 'HeatingThermostatPlus', 3 => 'WallMountedThermostat', 4 => 'ShutterContact', 5 => 'PushButton', 6 => 'virtualShutterContact', 7 => 'virtualThermostat', 8 => 'PlugAdapter' ); 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 ] ); 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 ($name, undef, $type, $addr) = split(m{ \s+ }xms, $def, 4); return "name $name is reserved for internal use" if (($name eq 'fakeWallThermostat') || ($name eq 'fakeShutterContact')); my $devtype = MAX_TypeToTypeId($type); return "$name, invalid MAX type $type !" if ($devtype < 0); return "$name, invalid address $addr !" if (($addr !~ m{\A[a-fA-F0-9]{6}\z}xms) || ($addr eq '000000')); $addr = lc($addr); # all addr should be lowercase if (exists($modules{MAX}{defptr}{$addr}) && $modules{MAX}{defptr}{$addr}->{NAME} ne $name) { my $msg = "MAX_Define, a MAX device with address $addr is already defined as ".$modules{MAX}{defptr}{$addr}->{NAME}; Log3($name, 2, $msg); return $msg; } my $old_addr = ''; # check if we have this address already in use foreach my $dev ( keys %{$modules{MAX}{defptr}} ) { next if (!$modules{MAX}{defptr}{$dev}->{NAME}); $old_addr = $dev if ($modules{MAX}{defptr}{$dev}->{NAME} eq $name); last if ($old_addr); # device found } if (($old_addr ne '') && ($old_addr ne $addr)){ my $msg1 = 'please dont change the address direct in DEF or RAW !'; my $msg2 = "If you want to change $old_addr please delete device $name first and create a new one"; Log3($name, 3, "$name, $msg1 $msg2"); return $msg1."\n".$msg2; } if (exists($modules{MAX}{defptr}{$addr}) && $modules{MAX}{defptr}{$addr}->{type} ne $type) { my $msg = "$name, type changed from $modules{MAX}{defptr}{$addr}->{type} to $type !"; Log3($name, 2, $msg); } $hash->{type} = $type; $hash->{devtype} = $devtype; $hash->{addr} = $addr; $hash->{'.count'} = 0; # ToDo Kommentar $hash->{'.sendToAddr'} = '-1'; # zu wem haben wird direkt gesendet ? $hash->{'.sendToName'} = ''; $hash->{'.timer'} = 300 if (($type ne 'PushButton') && ($type ne 'Cube')); $hash->{SVN} = (qw($Id$))[2]; if ($type =~ m{Thermostat}xms) { $hash->{TimeSlot} = -1 ; # wird durch CUL_MAX neu gesetzt $hash->{webCmd} = 'desiredTemperature'; # Hint for FHEMWEB } $modules{MAX}{defptr}{$addr} = $hash; CommandAttr(undef,"$name model $type"); # Forum Stats werten nur attr model aus if (($init_done == 1) && (($hash->{devtype} > 0) && ($hash->{devtype} < 4) || ($type eq 'virtualThermostat'))) { #nur beim ersten define setzen: readingsBeginUpdate($hash); MAX_ReadingsVal($hash, 'groupid'); MAX_ReadingsVal($hash, 'windowOpenTemperature') if ($hash->{TYPE} eq 'virtualThermostat'); MAX_ParseWeekProfile($hash); readingsEndUpdate($hash, 0); my ($io) = devspec2array('TYPE=CUL_MAX'); ($io) = devspec2array('TYPE=MAXLAN') if (!$io); $attr{$name}{IODev} = $io if (!exists($attr{$name}{IODev}) && $io); $attr{$name}{room} = 'MAX' if (!exists($attr{$name}{room})); } if ($type ne 'Cube') { AssignIoPort($hash); } else { CommandAttr(undef, "$name dummy 1"); CommandDeleteAttr(undef, "$name IODev") if (exists($attr{$name}{IODev})); } RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5, 'MAX_Timer', $hash, 0) if (($type ne 'PushButton') && ($type ne 'Cube')); return; } sub MAX_Timer { my $hash = shift; my $name = $hash->{NAME}; if (!$init_done) { InternalTimer(gettimeofday()+5, 'MAX_Timer', $hash, 0); return; } $hash->{'.timer'} //= 0; return if ((int($hash->{'.timer'}) < 60) || IsDummy($name) || IsIgnored($name)); InternalTimer(gettimeofday() + $hash->{'.timer'}, 'MAX_Timer', $hash, 0); if (exists($hash->{IODevMissing})) { Log3($hash, 1, "$name, Missing IODEV, call AssignIOPort"); AssignIoPort($hash); } if (($hash->{TYPE} =~ m{Thermostat}xms) || ($hash->{TYPE} eq 'PlugAdapter')) { 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); } return; } if (($hash->{TYPE} =~ m{ShutterContact\z}xms) && 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}); } if ($attrName eq 'dummy') { $attr{$name}{scanTemp} = '0' if (AttrNum($name, 'scanTemp', 0) && int($attrVal)); } if ($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); } if ($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); } if ($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 $type = shift; foreach my $id (keys %device_types) { return $id if ($type eq $device_types{$id}); } return -1; } sub MAX_CheckIODev { my $hash = shift; return 'device has no valid IODev' if (!exists($hash->{IODev})); return 'device IODev has no TYPE' if (!exists($hash->{IODev}{TYPE})); return 'device IODev TYPE must be CUL_MAX or MAXLAN' if ($hash->{IODev}{TYPE} ne 'MAXLAN' && $hash->{IODev}{TYPE} ne 'CUL_MAX'); return 'can not send a command with this IODev (missing IODev->Send)' if (!exists($hash->{IODev}{Send})); return $hash->{IODev}{TYPE}; } sub MAX_SerializeTemperature { # 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" my $t = shift; return $t if ( ($t eq 'on') || ($t eq 'off') ); return 'off' if ( $t == 4.5 ); return 'on' if ( $t == 30.5 ); return sprintf('%2.1f', $t); } sub MAX_Validate { my $name = shift; my $val = shift // 999; return 0 if (!exists($readingDef{$name})); return $readingDef{$name}[0]->($val); } sub MAX_ReadingsVal { # Get a reading, validating it's current value (maybe forcing to the default if invalid) # "on" and "off" are converted to their numeric values my $hash = shift; my $reading = shift; my $newval = shift; my $name = $hash->{NAME}; my $bulk = (exists($hash->{'.updateTimestamp'})) ? 1 : 0; # readingsBulkUpdate ist aktiv, wird von fhem.pl gesetzt/gelöscht if (defined($newval)) { return if ($newval eq ''); ($bulk) ? readingsBulkUpdate($hash, $reading, $newval) : 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($name, 3, "$name, $err , forcing to $val"); # Save default value to READINGS readingsBeginUpdate($hash) if (!$bulk); readingsBulkUpdate($hash, $reading, $val); readingsBulkUpdate($hash, 'error', $err); readingsEndUpdate($hash,0) if (!$bulk); } return MAX_ParseTemperature($val); } sub MAX_ParseWeekProfile { my $hash = shift; my @lines; # 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 // '?'; my $dev = shift // ''; return if (IsDummy($name) || IsIgnored($name) || ($hash->{devtype} == 6)); my $backuped_devs = MAX_BackupedDevs($name); return if (!$backuped_devs); return "$name, get show_savedConfig : missing device name !" if (($cmd eq 'show_savedConfig') && !$dev); 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, $setting, @args) = @_; $setting // return "set $devname needs at least one argument !"; my $ret = ''; my $devtype = int($hash->{devtype}); return if (IsDummy($devname) || IsIgnored($devname) || !$devtype || ($setting eq 'valveposition') || (($setting eq 'temperature') && (($devtype != 7) || ($hash->{IODev}->{TYPE} ne 'CUL_MAX'))) ); if ($setting eq 'mode') { $setting = 'desiredTemperature' if ($args[0] eq 'auto'); 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','')); } return MAX_saveConfig($devname, $args[0]) if ($setting eq 'saveConfig'); return MAX_Save('all') if ($setting eq 'saveAll'); if (($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 MAXX_Restore($devname ,$setting, $f); } if ($setting eq 'deviceRename') { my $newName = $args[0]; return CommandRename(undef, "$devname $newName"); } if ($setting ne '?') { my $error = MAX_CheckIODev($hash); if (($error ne 'CUL_MAX') && ($error ne 'MAXLAN')) { Log3($hash, 2, "$devname, $error"); return $error; } } if (($setting eq 'desiredTemperature') && ($hash->{type} =~ m{Thermostat}xms)) { 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