diff --git a/fhem/FHEM/10_MAX.pm b/fhem/FHEM/10_MAX.pm index c6d077abd..935151998 100755 --- a/fhem/FHEM/10_MAX.pm +++ b/fhem/FHEM/10_MAX.pm @@ -1,102 +1,181 @@ -############################################## # $Id$ -# Written by Matthias Gehre, M.Gehre@gmx.de, 2012-2013 +# +# (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 MIME::Base64; -use MaxCommon; 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" +); -sub MAX_Define($$); -sub MAX_Undef($$); -sub MAX_Initialize($); -sub MAX_Parse($$); -sub MAX_Set($@); -sub MAX_MD15Cmd($$$); -sub MAX_DateTime2Internal($); -sub MAX_DbLog_splitFn($); +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 %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; -sub validWindowOpenDuration { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 60; } -sub validMeasurementOffset { return $_[0] =~ /^-?\d+(\.[05])?$/ && $_[0] >= -3.5 && $_[0] <= 3.5; } -sub validBoostDuration { return $_[0] =~ /^\d+$/ && exists($boost_durationsInv{$_[0]}); } -sub validValveposition { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 100; } -sub validDecalcification { my ($decalcDay, $decalcHour) = ($_[0] =~ /^(...) (\d{1,2}):00$/); - return defined($decalcDay) && defined($decalcHour) && exists($decalcDaysInv{$decalcDay}) && 0 <= $decalcHour && $decalcHour < 24; } -sub validWeekProfile { return length($_[0]) == 4*13*7; } -sub validGroupid { return $_[0] =~ /^\d+$/ && $_[0] >= 0 && $_[0] <= 255; } - my %readingDef = ( #min/max/default - "maximumTemperature" => [ \&validTemperature, "on"], - "minimumTemperature" => [ \&validTemperature, "off"], - "comfortTemperature" => [ \&validTemperature, 21], - "ecoTemperature" => [ \&validTemperature, 17], - "windowOpenTemperature" => [ \&validTemperature, 12], - "windowOpenDuration" => [ \&validWindowOpenDuration, 15], - "measurementOffset" => [ \&validMeasurementOffset, 0], - "boostDuration" => [ \&validBoostDuration, 5 ], - "boostValveposition" => [ \&validValveposition, 80 ], - "decalcification" => [ \&validDecalcification, "Sat 12:00" ], - "maxValveSetting" => [ \&validValveposition, 100 ], - "valveOffset" => [ \&validValveposition, 00 ], - "groupid" => [ \&validGroupid, 0 ], - ".weekProfile" => [ \&validWeekProfile, $defaultWeekProfile ], -); + "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" - ); +#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_Initialize($) + +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) = @_; + my ($hash) = shift; - Log3 $hash, 5, "Calling MAX_Initialize"; - $hash->{Match} = "^MAX"; - $hash->{DefFn} = "MAX_Define"; - $hash->{UndefFn} = "MAX_Undef"; - $hash->{ParseFn} = "MAX_Parse"; - $hash->{SetFn} = "MAX_Set"; - $hash->{AttrList} = "IODev do_not_notify:1,0 ignore:0,1 dummy:0,1 " . - "showtime:1,0 keepAuto:0,1 scanTemp:0,1 model:HeatingThermostat,HeatingThermostatPlus,WallMountedThermostat,ShutterContact,PushButton ". - $readingFnAttributes; + $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"; - return undef; + $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($$) +sub MAX_Define { - my ($hash, $def) = @_; - my @a = split("[ \t][ \t]*", $def); + 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(@a)!=4 || $a[3] !~ m/^[a-fA-F0-9]{6}$/i); - return 'incorrect address 000000' if ($a[3] eq '000000'); + 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 - my $type = $a[2]; - my $addr = lc($a[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}.')'; @@ -104,54 +183,181 @@ MAX_Define($$) return $msg; } + return $type." is not a valid MAX type !" if (!MAX_TypeToTypeId($type) && ($type ne 'Cube')); - if($type eq "Cube") { - my $msg = "MAX_Define: Device type 'Cube' is deprecated. All properties have been moved to the MAXLAN device."; - Log3 $hash, 1, $msg; - return $msg; - } - Log3 $hash, 5, "Max_define $type with addr $addr "; - $hash->{type} = $type; - $hash->{devtype} = MAX_TypeToTypeId($type); - $hash->{addr} = $addr; + 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}; + #$hash->{internals}{interfaces} = $interfaces{$type}; # wozu ? AssignIoPort($hash); CommandAttr(undef,$name.' model '.$type); # Forum Stats werten nur attr model aus - return undef; -} - -sub -MAX_Undef($$) -{ - my ($hash,$name) = @_; - delete($modules{MAX}{defptr}{$hash->{addr}}); - return undef; -} - -sub -MAX_DateTime2Internal($) -{ - my($day, $month, $year, $hour, $min) = ($_[0] =~ /^(\d{2}).(\d{2})\.(\d{4}) (\d{2}):(\d{2})$/); - return (($month&0xE) << 20) | ($day << 16) | (($month&1) << 15) | (($year-2000) << 8) | ($hour*2 + int($min/30)); -} - -sub -MAX_TypeToTypeId($) -{ - foreach (keys %device_types) { - return $_ if($_[0] eq $device_types{$_}); + 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{$_}); } - Log 1, "MAX_TypeToTypeId: Invalid type $_[0]"; return 0; } -sub -MAX_CheckIODev($) + +sub MAX_CheckIODev { my $hash = shift; return !defined($hash->{IODev}) || ($hash->{IODev}{TYPE} ne "MAXLAN" && $hash->{IODev}{TYPE} ne "CUL_MAX"); @@ -159,55 +365,87 @@ MAX_CheckIODev($) # 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($) + +#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 { - if($_[0] eq "on" or $_[0] eq "off") { - return $_[0]; - } elsif($_[0] == 4.5) { - return "off"; - } elsif($_[0] == 30.5) { - return "on"; - } else { - return sprintf("%2.1f",$_[0]); - } + 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(@) +sub MAX_Validate # Todo : kann das weg ? { - my ($name,$val) = @_; - return 1 if(!exists($readingDef{$name})); + 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,$name) = @_; +# 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 $val = ReadingsVal($hash->{NAME},$name,""); - #$readingDef{$name} array is [validatingFunc, defaultValue] - if(exists($readingDef{$name}) and !$readingDef{$name}[0]->($val)) { +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 - Log3 $hash, 2, "MAX: Invalid value $val for READING $name on $hash->{NAME}. Forcing to $readingDef{$name}[1]"; - $val = $readingDef{$name}[1]; + 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($hash,$name,$val); - } else { - readingsSingleUpdate($hash,$name,$val,0); + 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); + return MAX_ParseTemperature($val); # ToDo : nochmal alle Aufrufe duchsehen ob das hier Sinn macht } -sub -MAX_ParseWeekProfile(@) { - my ($hash ) = @_; +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; @@ -216,291 +454,537 @@ MAX_ParseWeekProfile(@) { # 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++) { + 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++) { + 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; - for($j=0;$j<13;$j++) { + 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) { + if (int($hours[$j]) == 0 && int($minutes[$j]) == 0) + { $hours[$j] = 24; last; } } + my $time_prof_str = "00:00"; my $temp_prof_str; - for (my $k=0;$k<=$j;$k++) { + 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]); - if ($k < $j) { + + 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 .=','; } - } - readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-time", $time_prof_str ); - readingsBulkUpdate($hash, "weekprofile-$i-$decalcDays{$i}-temp", $temp_prof_str ); + } + 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($) +sub MAX_WakeUp { - my $hash = $_[0]; + 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_Set($@) +sub MAX_Get { - my ($hash, $devname, @a) = @_; - my ($setting, @args) = @a; + 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 undef if (IsDummy($devname) || IsIgnored($devname) || !$devtype || ($setting eq 'valveposition') || ($setting eq 'temperature')); + return if (IsDummy($devname) || IsIgnored($devname) || !$devtype || ($setting eq 'valveposition')); - return "Invalid IODev" if(MAX_CheckIODev($hash)); + 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 "desiredTemperature" and $hash->{type} =~ /.*Thermostat.*/) { - return "missing a value" if(@args == 0); + 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 + 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 + 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 - #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") { + if(@args == 2) + { + if($args[1] eq "eco") + { $temperature = MAX_ReadingsVal($hash,"ecoTemperature"); - } elsif($args[1] eq "comfort") { + } + elsif($args[1] eq "comfort") + { $temperature = MAX_ReadingsVal($hash,"comfortTemperature"); - } else { + } + else + { $temperature = MAX_ParseTemperature($args[1]); } - } elsif(@args == 1) { - $temperature = 0; #use temperature from weekly program - } else { - return "Too many parameters: desiredTemperature auto []"; + } + elsif(@args == 1) + { + $temperature = 0; # use temperature from weekly program + } + else + { + return $devname.', too many parameters: desiredTemperature auto []'; } - $ctrlmode = 0; #auto - } elsif($args[0] eq "boost") { - return "Too many parameters: desiredTemperature boost" if(@args > 1); + } # 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 + } + else + { + if($args[0] eq "manual") + { + # User explicitly asked for manual mode $ctrlmode = 1; #manual, possibly overwriting keepAuto shift @args; - return "Not enough parameters after 'desiredTemperature manual'" if(@args == 0); + return $devname.', not enough parameters after desiredTemperature manual' if(!@args); - } elsif(AttrVal($hash->{NAME},"keepAuto","0") ne "0" - && MAX_ReadingsVal($hash,"mode") eq "auto") { - #User did not ask for any mode explicitly, but has keepAuto - Log3 $hash, 5, "MAX_Set: staying in auto mode"; - $ctrlmode = 0; #auto + } + 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 { + 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 "Second parameter must be 'until'" if($args[1] ne "until"); - return "Not enough parameters: desiredTemperature [manual] [until