# # # 93_PWMR.pm # written by Andreas Goebel 2012-07-25 # e-mail: ag at goebel-it dot de # ############################################## # $Id: # 29.07.15 GA change set <name> manualTempDuration <minutes> # 21.09.15 GA update, use Log3 and readingsSingleUpdate # 07.10.15 GA initial version published # 07.10.15 GA fix calculation of PWMPulse, default for c_autoCalcTemp # 13.10.15 GA add event-on-change-reading # 14.10.15 GA fix round energyusedp # 15.10.15 GA add a_regexp_on, a regular expression for the on state of the actor # 05.11.15 GA fix new reading desired-temp-until which substitutes modification date of desired-temp in the future # events for desired-temp adjusted (no update of timestamp if temperature stays the same) # 10.11.15 GA fix event for actor change added again, desired-temp notifications adjusted for midnight change # 17.11.15 GA add ReadRoom will now set a reading named temperature containing the last temperature used for calculation # 18.11.15 GA add adjusted energyusedp to be in percent. Now it can be used in Tablet-UI as valve-position # 19.11.15 GA fix move actorState to readings # 22.11.15 GA fix rules on wednesday are now possible (thanks to Skusi) # 22.11.15 GA fix error handling in SetRoom (thanks to cobra112) # 30.11.15 GA fix set reading of desired-temp-used to frost_protect if window is opened # 30.11.15 GA add call PWMR_Attr in PWMR_Define if already some attributes are defined # 26.01.16 GA fix don't call AssignIoPort # 26.01.16 GA fix assign IODev as reference to that hash (otherwise xmllist will crash fhem) # 26.01.16 GA add implementation of PID regulation # 27.01.16 GA add attribute desiredTempFrom to take desiredTemp from another object # 04.02.16 GA add DLookBackCnt, buffer holding previouse temperatures used for PID D-Part calculation # 08.02.16 GA add ILookBackCnt, buffer holding previouse temperatures used for PID I-Part calculation # module for PWM (Pulse Width Modulation) calculation # this module defines a room for calculation # it is used by a PWM object # reference to the PWM object is via IODev # PWMR object defines: # IODev: reference to PWM # factor (also used in Pulse calculation): # temperatur difference * factor * cycletime (from PWM) defines on/off periods (pulse) # sensor delivering the temperature (temperature is read from reading using a regexp) # actor to switch on/off the heating devices (may be a structure if more than on actor..) # comma separated list of window contacts followd by ":" and a regular expression # default for the regular expression is "Open" # if the regular expression matches on of the contacts # then readRoom will return c_tempFrostProtect as desired-temp # instead of the current calculated desired-temp # this should cause the calculation routine for the room to switch off heating # # calculation of "desired-temp" is done in a loop (5-minutes default) # - if c_frostProtect is "1" -> set to c_tempFrostProtect # - if c_autoCalcTemp is "1" -> use c_tempN, c_tempD, c_tempC, c_tempE and c_tempRule[1-5] # - c_* variables are syntax checked and derived from attr which have a readable syntax # # - c_tempRule[1-5] are processed in order 5..1 (5 is highes priority) # rules define: # <interval of valid days (0..6 = So..Sa) # <time>,[N|D|C] [<time>,[N|D|C|E]] # ... time is interpreted as Hour[:Min] # ... N,D,C,E reference timeN (Night), timeD (Day), timeC (Cosy), timeE(Energysave) # # attr names are: tempDay, tempCosy, tempNight, tempEnergy ... # # subroutines PWMR_ReedRoom and PWMR_SetRoom are called from PWM object package main; use strict; use warnings; my %dayno = ( "mo" => 1, "di" => 2, "mi" => 3, "do" => 4, "fr" => 5, "sa" => 6, "so" => 0 ); sub PWMR_Get($@); sub PWMR_Set($@); sub PWMR_Define($$); sub PWMR_CalcDesiredTemp($); sub PWMR_SetRoom(@); sub PWMR_ReadRoom(@); sub PWMR_Attr(@); sub PWMR_Boost(@); ################################### sub PWMR_Initialize($) { my ($hash) = @_; $hash->{GetFn} = "PWMR_Get"; $hash->{SetFn} = "PWMR_Set"; $hash->{DefFn} = "PWMR_Define"; $hash->{UndefFn} = "PWMR_Undef"; $hash->{AttrFn} = "PWMR_Attr"; $hash->{AttrList} = "disable:1,0 loglevel:0,1,2,3,4,5 event-on-change-reading ". "frostProtect:0,1 ". "autoCalcTemp:0,1 ". "desiredTempFrom ". "tempFrostProtect ". "tempDay ". "tempNight ". "tempCosy ". "tempEnergy ". "tempRule1 ". "tempRule2 ". "tempRule3 ". "tempRule4 ". "tempRule5 ". ""; } sub PWMR_getDesiredTempFrom(@) { my ($hash, $dt, $d_reading, $d_regexpTemp) = @_; my $newTemp; my $d_readingVal = defined($dt->{READINGS}{$d_reading}{VAL}) ? $dt->{READINGS}{$d_reading}{VAL} : "undef"; my $val = $d_readingVal; $val =~ /$d_regexpTemp/; if (defined($1)) { $newTemp = $1; Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val)"); } else { $newTemp = $hash->{c_tempFrostProtect}; Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val) set to frostProtect"); } Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val)"); return ($newTemp); } ################################### sub PWMR_CalcDesiredTemp($) { my ($hash) = @_; if($hash->{INTERVAL} > 0) { if ($hash->{INTERVAL} == 300) { # align interval to hh:00:ss, hh:05:ss, ... hh:55:ss my $n = gettimeofday(); my ($hour, $min, $sec) = split (":", FmtTime($n)); # 15:12:05 -> 15:16:05 my $offset = ((((int($min/ 5)) +1 ) * 5 ) - $min) * 60; #Log3 ($hash, 4, "offset $min -> ".int($min / 5)." $offset ".($offset / 60)); InternalTimer($n + $offset, "PWMR_CalcDesiredTemp", $hash, 0); } else { InternalTimer(gettimeofday()+$hash->{INTERVAL}, "PWMR_CalcDesiredTemp", $hash, 0); #Log3 ($hash, 4, "interval not 300"); } } my $name = $hash->{NAME}; if (defined($hash->{READINGS}{"desired-temp-until"})) { if ($hash->{READINGS}{"desired-temp-until"}{VAL} ne "no" ) { if ($hash->{READINGS}{"desired-temp-until"}{VAL} gt TimeNow()) { Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: desired-temp was manualy set until ". $hash->{READINGS}{"desired-temp"}{TIME}); $hash->{STATE} = "ManualSetUntil"; return undef; } else { readingsSingleUpdate ($hash, "desired-temp-until", "no", 1); Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: calc desired-temp"); } } } #if ($hash->{READINGS}{"desired-temp"}{TIME} gt TimeNow()) { # Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: desired-temp was manualy set until ". # $hash->{READINGS}{"desired-temp"}{TIME}); # # $hash->{STATE} = "ManualSetUntil"; # return undef; #} else { # Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: calc desired-temp"); #} #################### # frost protection if ($hash->{c_frostProtect} > 0) { if ($hash->{READINGS}{"desired-temp"}{VAL} ne $hash->{c_tempFrostProtect} or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) { readingsSingleUpdate ($hash, "desired-temp", $hash->{c_tempFrostProtect}, 1); } else { readingsSingleUpdate ($hash, "desired-temp", $hash->{c_tempFrostProtect}, 0); } #$hash->{READINGS}{"desired-tem"}{TIME} = TimeNow(); #$hash->{READINGS}{"desired-temp"}{VAL} = $hash->{c_tempFrostProtect}; #push @{$hash->{CHANGED}}, "desired-temp $hash->{c_tempFrostProtect}"; #DoTrigger($name, undef); $hash->{STATE} = "FrostProtect"; return undef; } #################### # rule based calculation if ($hash->{c_autoCalcTemp} > 0 ) { if ($hash->{c_desiredTempFrom} eq "") { $hash->{STATE} = "Calculating"; my @time = localtime(); my $wday = $time[6]; my $cmptime = sprintf ("%02d%02d", $time[2], $time[1]); Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: wday $wday cmptime $cmptime"); foreach my $rule ($hash->{c_tempRule5}, $hash->{c_tempRule4}, $hash->{c_tempRule3}, $hash->{c_tempRule2}, $hash->{c_tempRule1} ) { if ($rule ne "") { # valid rule is 1-5 0600,D 1800,C 2200,N Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: $rule"); my @points = split (" ", $rule); my ($dayfrom, $dayto) = split ("-", $points[0]); #Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: dayfrom $dayfrom dayto $dayto"); my $rulematch = 0; if ($dayfrom <= $dayto ) { # rule 1-5 or 4-4 $rulematch = ($wday >= $dayfrom && $wday <= $dayto); } else { # rule 5-2 $rulematch = ($wday >= $dayfrom || $wday <= $dayto); } if ($rulematch) { for (my $i=int(@points)-1; $i>0; $i--) { Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: i:$i $points[$i]"); my ($ruletime, $tempV) = split (",", $points[$i]); if ($cmptime >= $ruletime) { my $temperature = $hash->{"c_tempN"}; $temperature = $hash->{"c_tempD"} if ($tempV eq "D"); $temperature = $hash->{"c_tempC"} if ($tempV eq "C"); $temperature = $hash->{"c_tempE"} if ($tempV eq "E"); Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: match i:$i $points[$i] ($tempV/$temperature)"); if ($hash->{READINGS}{"desired-temp"}{VAL} ne $temperature or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) { readingsSingleUpdate ($hash, "desired-temp", $temperature, 1); } else { readingsSingleUpdate ($hash, "desired-temp", $temperature, 0); } #$hash->{READINGS}{"desired-temp"}{TIME} = TimeNow(); #$hash->{READINGS}{"desired-temp"}{VAL} = $temperature; #push @{$hash->{CHANGED}}, "desired-temp $temperature"; #DoTrigger($name, undef); return undef; } } # no interval matched .. guess I am before the first one # so I choose the temperature from yesterday :-) # this should be the tempN my $newTemp = $hash->{"c_tempN"}; my $act_dtemp = $hash->{READINGS}{"desired-temp"}{VAL}; Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: use last value ($act_dtemp)"); if ($act_dtemp ne $newTemp or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) { readingsSingleUpdate ($hash, "desired-temp", $newTemp, 1); #} else { # readingsSingleUpdate ($hash, "desired-temp", $newTemp, 0); } #$hash->{READINGS}{"desired-temp"}{TIME} = TimeNow(); #$hash->{READINGS}{"desired-temp"}{VAL} = $newTemp; #push @{$hash->{CHANGED}}, "desired-temp $newTemp"; #DoTrigger($name, undef); return undef; } } } } else { # $hash->{c_desiredTempFrom} is set $hash->{STATE} = "From $hash->{d_name}"; my $newTemp = PWMR_getDesiredTempFrom ($hash, $defs{$hash->{d_name}}, $hash->{d_reading}, $hash->{d_regexpTemp}); if ($hash->{READINGS}{"desired-temp"}{VAL} ne $newTemp or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) { readingsSingleUpdate ($hash, "desired-temp", $newTemp, 1); } else { readingsSingleUpdate ($hash, "desired-temp", $newTemp, 0); } } } else { $hash->{STATE} = "Manual"; } #DoTrigger($name, undef); return undef; } ################################### sub PWMR_Get($@) { my ($hash, @a) = @_; return "argument is missing" if(int(@a) != 2); my $msg; if($a[1] ne "status") { return "unknown get value, valid is status"; } $hash->{LOCAL} = 1; RemoveInternalTimer($hash); my $v = PWMR_CalcDesiredTemp($hash); delete $hash->{LOCAL}; return "$a[0] $a[1] => recalculatd"; } ############################# sub PWMR_Set($@) { my ($hash, @a) = @_; my $name = $hash->{NAME}; my @list = map { ($_.".0", $_+0.5) } (6..29); my $valList = join (",", @list); $valList .= ",30.0"; #my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:knob,min:6,max:26,step:0.5,linecap:round interval manualTempDuration:slider,60,60,600"; #my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:uzsuDropDown:$valList interval manualTempDuration:slider,60,60,600"; my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:$valList interval manualTempDuration:slider,60,60,600"; $valList = "slider,6,0.5,30,0.5"; return $u if ($a[1] eq "?"); return $u if(int(@a) < 3); my $cmd = $a[1]; ############## # manualTempDuration if ( $cmd eq "manualTempDuration" ) { readingsSingleUpdate ($hash, "manualTempDuration", $a[2], 1); #$hash->{READINGS}{"manualTempDuration"}{VAL} = $a[2]; #$hash->{READINGS}{"manualTempDuration"}{TIME} = TimeNow(); return undef; } ############## # desired-temp if ( $cmd eq "desired-temp" ) { my $val = $a[2]; if ( $val < 6 || $val > 30 ) { return "Unknown argument for $cmd, choose <6..30>"; } my $duration = defined($hash->{READINGS}{"manualTempDuration"}{VAL}) ? $hash->{READINGS}{"manualTempDuration"}{VAL} * 60 : 60 * 60; if (defined($a[3])) { $duration = int($a[3]) * 60; } # manual set desired-temp will be set for 1 hour (default) # afterwards it will be overwritten by auto calc my $now = time(); readingsBeginUpdate ($hash); readingsBulkUpdate ($hash, "desired-temp", $a[2]); if ($hash->{c_autoCalcTemp} == 0) { $hash->{STATE} = "Manual"; } else { $hash->{STATE} = "ManualSetUntil"; readingsBulkUpdate ($hash, "desired-temp-until", FmtDateTime($now + $duration)); } readingsEndUpdate($hash, 1); #readingsSingleUpdate ($hash, "desired-temp", $a[2], 1); #$hash->{READINGS}{$cmd}{TIME} = FmtDateTime($now + $duration); #$hash->{READINGS}{$cmd}{VAL} = $val; #push @{$hash->{CHANGED}}, "$cmd: $val"; #DoTrigger($hash, undef); return undef } ############## # actor if ( $cmd eq "actor" ) { my $val = $a[2]; if ( $val eq "on" || $val eq "off" ) { PWMR_SetRoom($hash, $val); return undef; } else { return "Unknow argument for $cmd, choose on|off"; } } ############## # others if ($cmd =~ /^interval$|^factor$/) { my $var = uc($a[1]); $hash->{$var} = $a[2]; } else { return $u; } return undef; } ############################# sub PWMR_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $name = $hash->{NAME}; return "syntax: define <name> PWMR <IODev> <factor[,offset]> <tsensor[:reading[:t_regexp]]> <actor>[:<a_regexp_on>] [<window|dummy>[,<window>][:<w_regexp>]] [<usePID 0|1>:<PFactor>:<IFactor>[,<ILookBackCnt>]:<DFactor>[,<DLookBackCnt>]]" if(int(@a) < 6 || int(@a) > 9); my $iodevname = $a[2]; my $factor = ((int(@a) > 2) ? $a[3] : 0.2); my $tsensor = ((int(@a) > 3) ? $a[4] : ""); my $actor = ((int(@a) > 4) ? $a[5] : ""); my $window = ((int(@a) > 6) ? $a[6] : ""); my $pid = ((int(@a) > 7) ? $a[7] : ""); my ($f, $o) = split (",", $factor, 2); $o = 0.11 unless (defined ($o)); # if cycletime is 900 then this increases the on-time by 1:39 (=99 seconds) $hash->{TEMPSENSOR} = $tsensor; $hash->{ACTOR} = $actor; $hash->{WINDOW} = ($window eq "dummy" ? "" : $window); $hash->{FACTOR} = $f; # pulse is calculated using the below formular $hash->{FOFFSET} = $o; # ( $deltaTemp * $factor) ** 2) + $factoroffset $hash->{c_desiredTempFrom} = ""; #$hash->{helper}{cycletime} = 0; if ( !$iodevname ) { return "unknown device $iodevname"; } if ( $defs{$iodevname}->{TYPE} ne "PWM" ) { return "wrong type of $iodevname (not PWM)"; } #$hash->{IODev} = $iodev; $hash->{IODev} = $defs{$iodevname}; ########## # calculage factoroffset # 01.10.2015 #my $minonoff = $defs{$iodev}->{MINONOFFTIME}; #my $cycle = $defs{$iodev}->{CYCLETIME}; #my $factorOffset = ($minonoff / $cycle) - 0.02; #$factorOffset = sprintf ("%.2f", $factorOffset); #$hash->{factoroffset} = $factorOffset; ########## # check window $hash->{windows} = ""; my ($allwindows, $w_regexp) = split (":", $window, 2); if ( !defined($w_regexp) ) { # this regexp defines the result of ReadRoom # if any window is open return 1 $w_regexp = '.*Open.*' } $hash->{w_regexp} = $w_regexp; if ( defined ($allwindows) ) { my (@windows) = split (",", $allwindows); foreach my $onewindow (@windows) { if (!$defs{$onewindow} && $onewindow ne "dummy") { my $msg = "$name: Unknown window device $onewindow specified"; Log3 ($hash, 3, "PWMR_Define $msg"); return $msg; } if (length($hash->{windows}) > 0 ) { $hash->{windows} .= ",$onewindow" } else { $hash->{windows} = "$onewindow" } } } ########## # check pid definition my ($usePID, $PFactor, $IFactorTemp, $DFactorTemp) = split (":", $pid, 5); $IFactorTemp = "0,1" unless (defined ($IFactorTemp)); $DFactorTemp = "0,1"unless (defined ($DFactorTemp)); my ($IFactor, $ILookBackCnt) = split (",", $IFactorTemp, 2); my ($DFactor, $DLookBackCnt) = split (",", $DFactorTemp, 2); $hash->{c_PID_useit} = !defined($usePID) ? -1 : $usePID; $hash->{c_PID_PFactor} = !defined($PFactor) ? 0 : $PFactor; $hash->{c_PID_IFactor} = !defined($IFactor) ? 0 : $IFactor; $hash->{c_PID_DFactor} = !defined($DFactor) ? 0 : $DFactor; $hash->{c_PID_ILookBackCnt} = !defined($ILookBackCnt) ? 3 : $ILookBackCnt; $hash->{c_PID_DLookBackCnt} = !defined($DLookBackCnt) ? 1 : $DLookBackCnt; $hash->{h_deltaTemp} = 0 unless defined ($hash->{h_deltaTemp}); $hash->{h_deltaTemp_D} = 0 unless defined ($hash->{h_deltaTemp_D}); #$hash->{h_pid_integrator} = 0; if ($pid eq "") { delete ($hash->{READINGS}{PID_PVal}) if (defined($hash->{READINGS}{PID_PVal})); delete ($hash->{READINGS}{PID_IVal}) if (defined($hash->{READINGS}{PID_IVal})); delete ($hash->{READINGS}{PID_DVal}) if (defined($hash->{READINGS}{PID_DVal})); delete ($hash->{READINGS}{PID_PWMPulse}) if (defined($hash->{READINGS}{PID_PWMPulse})); delete ($hash->{READINGS}{PID_PWMOnTime}) if (defined($hash->{READINGS}{PID_PWMOnTime})); delete ($hash->{helper}{PID_D_previousTemps}) if (defined (($hash->{helper}{PID_D_previousTemps}))); #delete ($hash->{h_deltaTemp}) if (defined($hash->{h_deltaTemp})); #delete ($hash->{h_pid_integrator}) if (defined($hash->{h_pid_integrator})); } else { ### I-Factor # initialize if not yet done $hash->{helper}{PID_I_previousTemps} = [] unless defined (($hash->{helper}{PID_I_previousTemps})); # shorter reference to array my $IBuffer = $hash->{helper}{PID_I_previousTemps}; my $Icnt = ( @{$IBuffer} ); # or scalar @{$IBuffer} # reference #Log3 ($hash, 3, "org reference IBuffer is $hash->{helper}{PID_I_previousTemps} short is $IBuffer, cnt is ". scalar @{$IBuffer}." (starting from 0)"); Log3 ($hash, 4, "content of IBuffer is @{$IBuffer}"); # cut Buffer if it is too large while (scalar @{$IBuffer} > $hash->{c_PID_DLookBackCnt}) { my $v = shift @{$IBuffer}; # Log3 ($hash, 3, "shift $v from IBuffer"); } # Log3 ($hash, 3, "IBuffer contains ".scalar @{$IBuffer}." elements"); ### D-Factor # initialize if not yet done $hash->{helper}{PID_D_previousTemps} = [] unless defined (($hash->{helper}{PID_D_previousTemps})); # shorter reference to array my $DBuffer = $hash->{helper}{PID_D_previousTemps}; my $Dcnt = ( @{$DBuffer} ); # or scalar @{$DBuffer} # reference #Log3 ($hash, 3, "org reference DBuffer is $hash->{helper}{PID_D_previousTemps} short is $DBuffer, cnt is ". scalar @{$DBuffer}." (starting from 0)"); Log3 ($hash, 4, "content of DBuffer is @{$DBuffer}"); # for my $i ( 0 .. $cnt -1 ) { # Log3 ($hash, 3, "value $i $DBuffer->[$i]"); # } # # push @{$DBuffer}, $DBuffer->[1] + $DBuffer->[0]; # # for my $i ( 0 .. @{$DBuffer} -1 ) { # Log3 ($hash, 3, "value after push $i $DBuffer->[$i]"); # } # # shift @{$DBuffer}; # # for my $i ( 0 .. @{$DBuffer} -1 ) { # Log3 ($hash, 3, "value after shift $i $DBuffer->[$i]"); # } # cut Buffer if it is too large while (scalar @{$DBuffer} > $hash->{c_PID_DLookBackCnt}) { my $v = shift @{$DBuffer}; # Log3 ($hash, 3, "shift $v from DBuffer"); } # Log3 ($hash, 3, "DBuffer contains ".scalar @{$DBuffer}." elements"); } ########## # check sensor # dummy is allowed and will be ignored my ($sensor, $reading, $t_regexp) = split (":", $tsensor, 3); if (!$defs{$sensor} && $sensor ne "dummy") { my $msg = "$name: Unknown sensor device $sensor specified"; Log3 ($hash, 3, "PWMR_Define $msg"); return $msg; } $sensor =~ s/dummy//; $hash->{t_sensor} = $sensor; $reading = "temperature" unless (defined($reading)); $hash->{t_reading} = $reading; if ( !defined($t_regexp) ) { $t_regexp = '([\\d\\.]+)' } $hash->{t_regexp} = $t_regexp; ########## # check actor my ($tactor, $a_regexp_on) = split (":", $actor, 2); $a_regexp_on = "on" unless defined ($a_regexp_on); $tactor =~ s/dummy//; if (!$defs{$tactor} && $tactor ne "dummy") { my $msg = "$name: Unknown actor device $tactor specified"; Log3 ($hash, 3, "PWMR_Define $msg"); return $msg; } $hash->{actor} = $tactor; $hash->{a_regexp_on} = $a_regexp_on; #$hash->{actorState} = "unknown"; readingsSingleUpdate ($hash, "actorState", "unknown", 0); $hash->{STATE} = "Initialized"; # values for calculation of desired-temp $hash->{c_frostProtect} = 0; $hash->{c_autoCalcTemp} = 1; $hash->{c_tempFrostProtect} = 6; $hash->{c_tempN} = 16; $hash->{c_tempD} = 20; $hash->{c_tempC} = 22; $hash->{c_tempE} = 19; $hash->{c_tempRule1} = "1-5 0600,D 2200,N"; $hash->{c_tempRule2} = "6-0 0800,D 2200,N"; $hash->{c_tempRule3} = ""; $hash->{c_tempRule4} = ""; $hash->{c_tempRule5} = ""; $hash->{INTERVAL} = 300; #AssignIoPort($hash); # if attributes already defined then recall set for them foreach my $oneattr (sort keys %{$attr{$name}}) { PWMR_Attr ("set", $name, $oneattr, $attr{$name}{$oneattr}); } if($hash->{INTERVAL}) { InternalTimer(gettimeofday()+10, "PWMR_CalcDesiredTemp", $hash, 0); } return undef; } ############################# sub PWMR_Undef($$) { my ($hash, $args) = @_; my $name = $hash->{NAME}; Log3 ($hash, 3, "PWMR Undef $name"); if ( $hash->{INTERVAL} ) { RemoveInternalTimer($hash); } return undef; } ############################# sub PWMR_SetRoom(@) { my ($room, $newState) = @_; my $name = $room->{NAME}; Log3 ($room, 4, "PWMR_SetRoom $name <$newState>"); my $energyused = ""; if (defined($room->{READINGS}{energyused}{VAL})) { $energyused = substr ( $room->{READINGS}{energyused}{VAL}, -29); } # newState may be "", "on", "off" if ($newState eq "") { $energyused = $energyused.substr ( $energyused ,-1); } else { $energyused = $energyused.($newState eq "on" ? "1" : "0"); } readingsBeginUpdate ($room); readingsBulkUpdate ($room, "energyused", $energyused); readingsBulkUpdate ($room, "energyusedp", sprintf ("%.1f", ($energyused =~ tr/1//) /30*100)); if ($newState eq "") { readingsEndUpdate($room, 1); return; } if ($room->{actor}) { my $ret = fhem sprintf ("set %s %s", $room->{actor}, $newState); if (!defined($ret)) { # sucessfull Log3 ($room, 2, "PWMR_SetRoom $room->{NAME}: set $room->{actor} $newState"); #$room->{actorState} = $newState; readingsBulkUpdate ($room, "actorState", $newState); readingsBulkUpdate ($room, "lastswitch", time()); readingsEndUpdate($room, 1); push @{$room->{CHANGED}}, "actor $newState"; DoTrigger($name, undef); } else { Log3 ($room, 2, "PWMR_SetRoom $name: set $room->{actor} $newState failed ($ret)"); } } } ################################### sub PWMR_ReadRoom(@) { my ($room, $cycletime, $MaxPulse) = @_; # room, cylcetime for PMW Calculation (15Min), Max Time to stay on (0.00 .. 1.00) my $name = $room->{NAME}; my $temperaturT; my $desiredTemp; my $prevswitchtimeT; #$room->{helper}{cycletime} = $cycletime; my ($temperaturV, $actorV, $factor, $oldpulse, $newpulse, $prevswitchtime, $windowV) = (99, "off", 0, 0, 0, 0, 0); #Log3 ($room, 4, "PWMR_ReadRoom $name <$room->{t_sensor}> <$room->{actor}>"); if ($room->{t_sensor}) { my $sensor = $room->{t_sensor}; my $reading = $room->{t_reading}; my $t_regexp = $room->{t_regexp}; $temperaturV = $defs{$sensor}->{READINGS}{$reading}{VAL}; $temperaturT = $defs{$sensor}->{READINGS}{$reading}{TIME}; $temperaturV =~ s/$t_regexp/$1/; } if ($room->{actor}) { # HERE #$actorV = (($defs{$room->{actor}}->{STATE} eq "on") : "on" ? "off"); #$actorV = $defs{$room->{actor}}->{STATE}; # until 26.01.2013 -> may be undef which forces room to be switched off first #$actorV = $room->{actorState}; # starting from 26.01.2013 -> try to read act status .. (may also be invalid if struct) if ($defs{$room->{actor}}->{TYPE} eq "RBRelais") { $actorV = $defs{$room->{actor}}->{STATE}; } elsif (defined($defs{$room->{actor}}->{STATE})) { $actorV = $defs{$room->{actor}}->{STATE}; } else { #$actorV = $room->{actorState}; $actorV = $room->{READINGS}{actorState}; } #my $actorVOrg = $actorV; my $a_regexp_on = $room->{a_regexp_on}; if ($actorV =~ /^$a_regexp_on$/) { $actorV = "on"; } else { $actorV = "off"; } #Log3 ($room, 2, "$name actorV $actorV org($actorVOrg) regexp($a_regexp_on)"); } if (!$room->{READINGS}{"desired-temp"}{TIME}) { readingsSingleUpdate ($room, "desired-temp", 6.0, 0); } if (!$room->{READINGS}{oldpulse}{TIME}) { readingsSingleUpdate ($room, "oldpulse", 0.0, 0); } if (!$room->{READINGS}{lastswitch}{TIME}) { readingsSingleUpdate ($room, "lastswitch", time(), 0); } $factor = $room->{FACTOR}; $oldpulse = $room->{READINGS}{oldpulse}{VAL}; $prevswitchtime = $room->{READINGS}{lastswitch}{VAL}; $prevswitchtimeT = $room->{READINGS}{lastswitch}{TIME}; $windowV = 0; if ($room->{windows} && $room->{windows} ne "" && $room->{w_regexp} ne "") { foreach my $window (split (",", $room->{windows})) { Log3 ($room, 4, "PWMR_ReadRoom $name: check window $window"); if (defined($room->{w_regexp}) && $room->{w_regexp} ne "") { if (defined($defs{$window}) && $defs{$window}{STATE} ) { Log3 ($room, 5, "PWMR_ReadRoom $name: $window ($defs{$window}{STATE}/$room->{w_regexp})"); if ( $defs{$window}{STATE} =~ /$room->{w_regexp}/ ) { $windowV = 1; Log3 ($room, 3, "PWMR_ReadRoom $name: $window state: set to 1"); } } } } } if ($windowV > 0) { $desiredTemp = $room->{c_tempFrostProtect}; } else { $desiredTemp = $room->{READINGS}{"desired-temp"}{VAL}; } my $deltaTemp = maxNum (0, $desiredTemp - $temperaturV); my $factoroffset = $room->{FOFFSET}; $newpulse = minNum ($MaxPulse, (( $deltaTemp * $factor) ** 2) + $factoroffset); # default 85% max ontime $newpulse = sprintf ("%.2f", $newpulse); my $PWMPulse = $newpulse * 100; my $PWMOnTime = sprintf ("%02s:%02s", int ($newpulse * $cycletime / 60), ($newpulse * $cycletime) % 60); my $iodev = $room->{IODev}; #if ($newpulse * $defs{$iodev}->{CYCLETIME} < $defs{$iodev}->{MINONOFFTIME}) { if ($newpulse * $iodev->{CYCLETIME} < $iodev->{MINONOFFTIME}) { $PWMPulse = 0; $PWMOnTime = "00:00"; } ### PID calculation my $DBuffer = $room->{helper}{PID_D_previousTemps}; push @{$DBuffer}, $temperaturV; my $IBuffer = $room->{helper}{PID_I_previousTemps}; push @{$IBuffer}, $temperaturV; # cut I-Buffer if it is too large while (scalar @{$IBuffer} > $room->{c_PID_ILookBackCnt}) { my $v = shift @{$IBuffer}; #Log3 ($room, 3, "shift $v from IBuffer"); } #Log3 ($room, 3, "IBuffer contains ".scalar @{$IBuffer}." elements"); # cut D-Buffer if it is too large while (scalar @{$DBuffer} > $room->{c_PID_DLookBackCnt}) { my $v = shift @{$DBuffer}; #Log3 ($room, 3, "shift $v from DBuffer"); } #Log3 ($room, 3, "DBuffer contains ".scalar @{$DBuffer}." elements"); $room->{h_PID_I_previousTemps} = join (" ", @{$IBuffer}); $room->{h_PID_D_previousTemps} = join (" ", @{$DBuffer}); my $deltaTempPID = $desiredTemp - $temperaturV; $room->{h_deltaTemp} = sprintf ("%.1f", -1 * $deltaTempPID); $room->{h_deltaTemp_D} = sprintf ("%.1f", -1 * ($desiredTemp - $DBuffer->[0])); my $ISum = 0; foreach my $t (@{$IBuffer}) { $ISum += ($desiredTemp - $t); } #$ISum = $ISum / scalar @{$IBuffer}; $ISum = $ISum; #my $deltaTempPID = $desiredTemp - $temperaturV; #my $IVal = $room->{c_PID_IFactor} * $deltaTempPID + $room->{h_pid_integrator}; my $PVal = $room->{c_PID_PFactor} * $deltaTemp; my $IVal = $room->{c_PID_IFactor} * $ISum; my $DVal = $room->{c_PID_DFactor} * ($room->{h_deltaTemp_D} - $room->{h_deltaTemp}); $PVal = minNum (1, sprintf ("%.2f", $PVal)); $IVal = minNum (1, sprintf ("%.2f", $IVal)); $DVal = minNum (1, sprintf ("%.2f", $DVal)); $IVal = maxNum (-1, $IVal); #$room->{h_pid_integrator} = $IVal; my $newpulsePID = ($PVal + $IVal + $DVal); $newpulsePID = minNum ($MaxPulse, sprintf ("%.2f", $newpulsePID)); $newpulsePID = maxNum (0, sprintf ("%.2f", $newpulsePID)); my $PWMPulsePID = $newpulsePID * 100; my $PWMOnTimePID = sprintf ("%02s:%02s", int ($newpulsePID * $cycletime / 60), ($newpulsePID * $cycletime) % 60); if ($PWMPulsePID * $iodev->{CYCLETIME} < $iodev->{MINONOFFTIME}) { $PWMPulsePID = 0; $PWMOnTimePID = "00:00"; } # end PID calculation if ($room->{c_PID_useit} >= 1) { $newpulse = $newpulsePID; #$PWMPulse = $PWMPulsePID; #$PWMOnTime = $PWMOnTimePID; } readingsBeginUpdate ($room); readingsBulkUpdate ($room, "desired-temp-used", $desiredTemp); readingsBulkUpdate ($room, "PWMOnTime", $PWMOnTime); readingsBulkUpdate ($room, "PWMPulse", $PWMPulse); readingsBulkUpdate ($room, "temperature", $temperaturV); if ($room->{c_PID_useit} >= 0) { readingsBulkUpdate ($room, "PID_PVal", $PVal); readingsBulkUpdate ($room, "PID_IVal", $IVal); readingsBulkUpdate ($room, "PID_DVal", $DVal); readingsBulkUpdate ($room, "PID_PWMPulse", $PWMPulsePID); readingsBulkUpdate ($room, "PID_PWMOnTime", $PWMOnTimePID); } readingsEndUpdate($room, 1); Log3 ($room, 4, "PWMR_ReadRoom $name: desT($desiredTemp), actT($temperaturV von($temperaturT)), state($actorV)"); Log3 ($room, 4, "PWMR_ReadRoom $name: newpulse($newpulse/$PWMOnTime), oldpulse($oldpulse), lastSW($prevswitchtime = $prevswitchtimeT), window($windowV)"); return ($temperaturV, $actorV, $factor, $oldpulse, $newpulse, $prevswitchtime, $windowV); } sub PWMR_normTime ($) { my ($time) = @_; my $hour = 0; my $minute = 0; #Log 4, "normTime $time"; $time =~ /^([0-9]+):*([0-9]*)$/; if (defined ($2) && ($2 ne "")) { # set $minute to 0 if time was only 6 $minute = $2; } if (defined ($1)) { # error if no hour given $hour = $1 } else { return undef; } #Log 4, "<$hour> <$minute>"; if ($hour < 0 || $hour > 23) { return undef; } if ($minute < 0 || $minute > 59) { return undef; } #Log 4, "uhrzeit $hour $minute"; return sprintf ("%02d%02d", $hour, $minute); } sub PWMR_CheckTempRule(@) { my ($hash, $var, $vals) = @_; my $name = $hash->{NAME}; my $valid = ""; my $usage = "usage: [Mo|Di|..|So[-Mo|-Di|..|-So] <zeit>,D|C|E|N [<zeit>,D|C|E|N] ]\n". "e.g. Mo-Fr 6:00,D 22,N\n". "or So 10,D 23,N"; Log3 ($hash, 4, "PWMR_CheckTempRule: $hash->{NAME} $var <$vals>"); my @points = split (" ", $vals); my $day = $points[0]; unless ( $day =~ /-/ ) { # normalise Mo to Mo-Mo $day = "$day-$day"; } # analyse Mo-Di my ($from, $to) = split ("-", $day); $from = lc ($from); $to = lc ($to); if (defined ($dayno{$from}) && defined ($dayno{$to})) { $valid .= "$dayno{$from}-$dayno{$to} "; } else { return $usage; } Log3 ($hash, 4, "PWMR_CheckTempRule: $name day valid: $valid"); shift @points; foreach my $point (@points) { #Log3 ($hash, 4, "loop: $point"); my ($from, $temp) = split(",", $point); $temp = uc($temp); unless ($temp eq "D" || $temp eq "N" || $temp eq "C" || $temp eq "E") { # valid temp return $usage; } #Log3 ($hash, 4, "loop: fromto: $fromto"); return $usage unless ( $from = PWMR_normTime($from) ); Log3 ($hash, 4, "PWMR_CheckTempRule: $name time valid: $from,$temp"); $valid .= "$from,$temp "; } Log3 ($hash, 4, "PWMR_CheckTempRule: $name $var <$valid>"); $hash->{$var} = $valid; return undef } sub PWMR_CheckTemp(@) { my ($hash, $var, $vals) = @_; my $error = "valid values are 0 ... 30"; my $name = $hash->{NAME}; Log3 ($hash, 4, "PWMR_CheckTemp: $name $var <$vals>"); if ($vals !~ /^[0-9]+\.{0,1}[0-9]*$/ ) { return "$error"; } else { } if ($vals < 0 || $vals > 30) { return "$error"; } $hash->{$var} = $vals; return undef; } sub PWMR_Attr(@) { my @a = @_; my $name = $a[1]; my $hash = $defs{$name}; my $attr = $a[2]; my $val = $a[3]; if ($a[0] eq "del") { if ($attr eq "tempRule1") { $hash->{c_tempRule1} = ""; } elsif ($attr eq "tempRule2") { $hash->{c_tempRule2} = ""; } elsif ($attr eq "tempRule3") { $hash->{c_tempRule3} = ""; } elsif ($attr eq "tempRule4") { $hash->{c_tempRule4} = ""; } elsif ($attr eq "tempRule5") { $hash->{c_tempRule5} = ""; } elsif ($attr eq "frostProtect") { $hash->{c_frostProtect} = 0; } elsif ($attr eq "desiredTempFrom") { $hash->{c_desiredTempFrom} = ""; delete($hash->{d_name}); delete($hash->{d_reading}); delete($hash->{d_regexpTemp}); } elsif ($attr eq "autoCalcTemp") { $hash->{c_autoCalcTemp} = 1; $hash->{STATE} = "Calculating"; } } if (!defined($val)) { Log3 ($hash, 4, "PWMR_Attr: $name, delete $attr ($val)"); return undef; } else { Log3 ($hash, 4, "PWMR_Attr: $name, $attr, $val"); } if ($attr eq "frostProtect") { # frostProtect 0/1 if ($val eq 0 or $val eq 1) { $hash->{c_frostProtect} = $val; } elsif ($val eq "") { $hash->{c_frostProtect} = 0; } else { return "valid values are 0 or 1"; } } elsif ($attr eq "autoCalcTemp") { # autoCalcTemp 0/1 if ($val eq 0) { $hash->{c_autoCalcTemp} = 0; $hash->{STATE} = "Manual"; } elsif ( $val eq 1) { $hash->{c_autoCalcTemp} = 1; $hash->{STATE} = "Calculating"; } elsif ($val eq "") { $hash->{c_autoCalcTemp} = 1; $hash->{STATE} = "Calculating"; } else { return "valid values are 0 or 1"; } } elsif ($attr eq "desiredTempFrom") { # desiredTempFrom $hash->{c_desiredTempFrom} = $val; my ( $d_name, $d_reading, $d_regexpTemp) = split (":", $val, 3); # set defaults $hash->{d_name} = (defined($d_name) ? $d_name : ""); $hash->{d_reading} = (defined($d_reading) ? $d_reading : "desired-temp"); $hash->{d_regexpTemp} = (defined($d_regexpTemp) ? $d_regexpTemp : '(\d[\d\\.]+)'); # check if device exist unless (defined($defs{$hash->{d_name}})) { return "error: $hash->{d_name} does not exist."; } } elsif ($attr eq "tempDay") { # tempDay return PWMR_CheckTemp($hash, "c_tempD", $val); } elsif ($attr eq "tempNight") { # tempNight return PWMR_CheckTemp($hash, "c_tempN", $val); } elsif ($attr eq "tempCosy") { # tempCosy return PWMR_CheckTemp($hash, "c_tempC", $val); } elsif ($attr eq "tempEnergy") { # tempEnergy return PWMR_CheckTemp($hash, "c_tempE", $val); } elsif ($attr eq "tempRule1") { # tempRule1 return PWMR_CheckTempRule($hash, "c_tempRule1", $val); } elsif ($attr eq "tempRule2") { # tempRule2 return PWMR_CheckTempRule($hash, "c_tempRule2", $val); } elsif ($attr eq "tempRule3") { # tempRule3 return PWMR_CheckTempRule($hash, "c_tempRule3", $val); } elsif ($attr eq "tempRule4") { # tempRule4 return PWMR_CheckTempRule($hash, "c_tempRule4", $val); } elsif ($attr eq "tempRule5") { # tempRule5 return PWMR_CheckTempRule($hash, "c_tempRule5", $val); } return undef; } sub PWMR_Boost(@) { my ($me, $outsideSensor, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) = @_; return undef unless defined ($defs{$me}->{NAME}); my $room = $defs{$me}; my $name = $room->{NAME}; my $outsideTemp = 99; if (defined($defs{$outsideSensor}->{READINGS}{temperature}{VAL})) { $outsideTemp = $defs{$outsideSensor}->{READINGS}{temperature}{VAL}; } if ($room->{t_sensor}) { my $sensor = $room->{t_sensor}; my $reading = $room->{t_reading}; my $t_regexp = $room->{t_regexp}; my $temperaturV = $defs{$sensor}->{READINGS}{$reading}{VAL}; $temperaturV =~ s/$t_regexp/$1/; my $desiredTemp = $room->{READINGS}{"desired-temp"}{VAL}; # boost necessary? if (($outsideTemp < $outsideMax) && ($temperaturV <= $desiredTemp - $deltaTemp)) { Log3 ($room, 3, "PWMR_Boost: $name ". "($outsideTemp, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) ". "temp($temperaturV) desired-temp($desiredTemp) -> boost"); my $now = time(); readingsBeginUpdate ($room); readingsBulkUpdate ($room, "desired-temp", $desiredTemp + $desiredOffset); readingsBulkUpdate ($room, "desired-temp-until", FmtDateTime($now + $boostDuration * 60)); readingsEndUpdate($room, 1); #$room->{READINGS}{"desired-temp"}{TIME} = FmtDateTime($now + $boostDuration * 60); #$room->{READINGS}{"desired-temp"}{VAL} = $desiredTemp + $desiredOffset; #my $t = $room->{READINGS}{"desired-temp"}{VAL}; #push @{$room->{CHANGED}}, "desired-temp $t"; #DoTrigger($name, undef); Log3 ($room, 4, "PWMR_Boost: $name ". "set desired-temp ".$room->{READINGS}{"desired-temp"}{TIME}." for ". $room->{READINGS}{"desired-temp"}{VAL}); } else { Log3 ($room, 3, "PWMR_Boost: $name ". "($outsideTemp, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) ". "temp($temperaturV) desired-temp($desiredTemp) -> do nothing"); } } else { Log3 ($room, 3, "PWMR_Boost: $name warning: no sensor."); } return undef; } 1; =pod =begin html <a name="PWMR"></a> <h3>PWMR</h3> <ul> <table> <tr><td> The PMWR module defines rooms to be used for calculation within module PWM.<br><br> PWM is based on Pulse Width Modulation which means valve position 70% is implemented in switching the device on for 70% and off for 30% in a given timeframe.<br> PWM defines a calculation unit and depents on objects based on PWMR which define the rooms to be heated.<br> PWMR objects calculate a desired temperature for a room based on several rules, define windows, a temperature sensor and an actor to be used to switch on/off heating. <br> </td></tr> </table> <b>Define</b> <ul> <code>define <name> PWMR <IODev> <factor[,offset]> <tsensor[:reading:[t_regexp]]> <actor>[:<a_regexp_on>] [<window|dummy>[,<window>[:<w_regexp>]] [<usePID 0|1>:<PFactor>:<IFactor>[,<ILookBackCnt>]:<DFactor>,[<DLookBackCnt>]<br></code> <br> Define a calculation object with the following parameters:<br> <ul> <li>IODev<br> Reference to an object of TYPE PWM. This object will switch on/off heating.<br> </li> <li>factor[,offset]<br> Pulse for PWM will be calculated as ((delta-temp * factor) ** 2) + offset.<br> <i>offset</i> defaults to 0.11<br> <i>factor</i> can be used to weight rooms.<br> </li> <li>tsensor[:reading[:t_regexp]]<br> <i>tsensor</i> defines the temperature sensor for the actual room temperature.<br> <i>reading</i> defines the reading of the temperature sensor. Default is "temperature"<br> <i>t_regexp</i> defines a regular expression to be applied to the reading. Default is '(\d[\d\.]+)'.<br> </li> <li>actor[:<a_regexp_on>]<br> The actor will be set to "on" of "off" to turn on/off heating.<br> <i>a_regexp_on</i> defines a regular expression to be applied to the state of the actor. Default is 'on". If state matches the regular expression it is handled as "on", otherwise "off"<br> </li> <li><window|dummy>[,<window>[:<w_regexp>]<br> <i>window</i> defines several window devices that can prevent heating to be turned on.<br> If STATE matches the regular expression then the desired-temp will be decreased to frost-protect temperature.<br> 'dummy' can be used as a neutral value for window and will be ignored when processing the configuration.<br> <i>w_regexp</i> defines a regular expression to be applied to the reading. Default is '.*Open.*'.<br> </li> <li><usePID 0|1>:<PFactor>:<IFactor>[,<ILookBackCnt>]:<DFactor>[,<DLookBackCnt>]<br> <i>usePID 0|1</i>: 0 .. calculate Pulse based on PID but do not use it. 1 .. calculate Pulse based on PID and use it.<br> <i>PFactor</i>: Konstant for P.<br> <i>IFactor</i>: Konstant for I.<br> <i>DFactor</i>: Konstant for D.<br> <i>ILookBackCnt</i>: Buffer size to store previous temperatures. For I calculation all values will be used. Default is 3.<br> <i>DLookBackCnt</i>: Buffer size to store previous temperatures. For D calculation actual and oldest temperature will be used. Default is 1.<br> Internals c_PID_PFactor, c_PID_IFactor, c_PID_DFactor and c_PID_useit will reflect the above configuration values.<br> Internals h_deltaTemp h_deltaTemp_D store the values needed for calculation of the next PID value.<br> Readings PID_DVal, PID_IVal, PID_PVal, PID_PWMOnTime and PID_PWMPulse will reflect the actual calculated PID values and Pulse.<br> </li> </ul> <br> Example:<br> <br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen</code><br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen windowKitchen1,windowKitchen2</code><br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen windowKitchen1,windowKitchen2:.*Open.*</code><br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen windowKitchen1,windowKitchen2</code> 0:0.8:1:0<br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen dummy 0:0.8:1:0</code><br> <code>define roomKitchen PWMR fh 1,0 tempKitchen relaisKitchen dummy 1:0.8:1:0</code><br> <br> </ul> <br> <b>Set </b> <ul> <li>factor<br> Temporary change of parameter <i>factor</i>. </li><br> <li>actor<br> Set the actor state for this room to <i>on</i> or <i>off</i>. This is only a temporary change that will be overwritten by PWM object. </li><br> <li>desired-temp<br> If <i>desired-temp</i> is automatically calculated (attribute <i>autoCalcTemp</i> not set or 1) then the desired temperature is set for a defined time.<br> Default for this period is 60 minutes, but it can be changed by attribute <i>autoCalcTemp</i>.<br> If <i>desired-temp</i> is not automatically calculated (attribute <i>autoCalcTemp</i> is 0) then this will set the actual target temperature.<br> </li><br> <li>manualTempDuration<br> Define the period how long <i>desired-temp</i> manually set will be valid. Default is 60 Minutes.<br> </li><br> <li>interval<br> Temporary change <i>INTERVAL</i> which defines how often <i>desired-temp</i> is calculated in autoCalcMode. Default is 300 seconds (5:00 Minutes). </li><br> </ul> <br> <b>Attributes</b> <ul> <li>frostProtect<br> Switch on (1) of off (0) frostProtectMode. <i>desired-temp</i> will be set to <i>tempFrostProtect</i> in autoCalcMode. </li><br> <li>autoCalcTemp<br> Switch on (1) of off (0) autoCalcMode. <i>desired-temp</i> will be set based on the below temperatures and rules in autoCalcMode.<br> Default is on. </li><br> <li>tempDay<br> Define day temperature. This will be referenced as "D" in the rules. </li><br> <li>tempNight<br> Define night temperature. This will be referenced as "N" in the rules. </li><br> <li>tempCosy<br> Define cosy temperature. This will be referenced as "C" in the rules. </li><br> <li>tempEnergy<br> Define energy saving temperature. This will be referenced as "E" in the rules. </li><br> <li>tempFrostProtect<br> Define temperature for frostProtectMode. See also <i>frostProtect</i>. </li><br> <li>tempRule1 ... tempRule5<br> Rule to calculate the <i>desired-temp</i> in autoCalcMode.<br> Format is: <weekday>[-<weekday] <time>,<temperatureSelector><br> weekday is one of Mo,Di,Mi,Do,Fr,Sa,So<br> time is in format hh:mm, e.g. 7:00 or 07:00<br> temperatureSelector is one of D,N,C,E<br> <br> Predefined are:<br> tempRule1: Mo-Fr 6:00,D 22:00,N<br> tempRule2: Sa-So 8:00,D 22:00,N<br> This results in tempDay 6:00-22:00 from Monday to Friday and tempNight outside this time window.<br> </li><br> <li>desiredTempFrom<br> This can be used as an alternative to the calculation of desired-temp based on the tempRules when autoCalcTemp is set to '1'.<br> If set correctly the desired-temp will be read from a reading of another device.<br> Format is <device>[:<reading>[:<regexp>]]<br> <i>device</i> defines the reference to the other object.<br> <i>reading</i> defines the reading that contains the value for desired-temp. Default is 'desired-temp'.<br> <i>regexp</i> defines a regular expression to extract the value used for 'desired-temp'. Default is '(\d[\d\.]+)'. If <i>regexp</i> does not match (e.g. reading is 'off') then tempFrostProtect is used.<br> Internals c_desiredTempFrom reflects the actual setting and d_name, d_reading und d_regexpTemp the values used.<br> If this attribute is used then state will change from "Calculating" to "From <device>".<br> Calculation of desired-temp is (like when using tempRules) based on the interval specified for this device (default is 300 seconds). </li><br> </ul> <br> </ul> =end html =cut