diff --git a/CHANGED b/CHANGED index 352e3af57..82c41194e 100644 --- a/CHANGED +++ b/CHANGED @@ -1,7 +1,9 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. - SVN - - feature: new module 00_RPII2C.pm, 52_I2C_PCA9532.pm, 52_I2C_PCF8574.pm, 52_I2C_SHT21.pm added (klausw) + - feature: new module 98_PID20.pm added (John / betateilchen) + - feature: new module 00_RPII2C.pm, 52_I2C_PCA9532.pm, 52_I2C_PCF8574.pm, + 52_I2C_SHT21.pm added (klausw) - change: module 71_LISTENLIVE.pm moved to contrib module 23_WEBTHERM.pm moved to contrib - change: module 98_PID.pm moved to contrib as preparation for diff --git a/FHEM/98_PID20.pm b/FHEM/98_PID20.pm new file mode 100644 index 000000000..e4d0d22d7 --- /dev/null +++ b/FHEM/98_PID20.pm @@ -0,0 +1,848 @@ +# $Id: 98_PID20.pm 3988 2013-11-06 10:00:00Z john $ +#################################################################################################### +# +# 98_PID20.pm +# The PID device is a loop controller, used to set the value e.g of a heating +# valve dependent of the current and desired temperature. +# +# This module is derived from the contrib/99_PID by Alexander Titzel. +# The framework of the module is derived from proposals by betateilchen. +# +# This file is part of fhem. +# +# Fhem 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. +# +# Fhem 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. +# +# You should have received a copy of the GNU General Public License +# along with fhem. If not, see . +# +# +# V 1.00.c +# 03.12.2013 - bugfix : pidActorLimitUpper wrong assignment +# V 1.00.d +# 09.12.2013 - verbose-level adjusted +# 20.12.2013 - bugfix: actorErrorPos: wrong assignment by pidCalcInterval-attribute, if defined +# V 1.00.e +# 01.01.2014 - fix: {helper}{actorCommand} assigned to an emptry string if not defined +# V 1.00.f +# 22.01.2014 fix:pidDeltaTreshold only int was assignable, now even float +# V 1.00.g +# 29.01.2014 fix:calculation of i-portion is independent from pidCalcInterval +# V 1.00.h +# 26.02.2014 fix:new logging format; adjusting verbose-levels +# +# 26.03.2014 (betateilchen) +# code review, pod added, removed old version info (will be provided via SVN) + +#################################################################################################### +package main; +use strict; +use warnings; +use feature qw/say switch/; +use vars qw(%defs); +use vars qw($readingFnAttributes); +use vars qw(%attr); +use vars qw(%modules); + +sub PID20_Calc($); + +######################################## +sub PID20_Log($$$) +{ + my ( $hash, $loglevel, $text ) = @_; + my $xline = (caller(0))[2]; + + my $xsubroutine = (caller(1))[3]; + my $sub = (split( ':', $xsubroutine ))[2]; + $sub = substr ($sub, 6); # without PID20 + + my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "PID20"; + Log3 $hash, $loglevel, "PID20 $instName: $sub.$xline " . $text; +} + +######################################## +sub PID20_Initialize($) +{ + my ($hash) = @_; + $hash->{DefFn} = "PID20_Define"; + $hash->{UndefFn} = "PID20_Undef"; + $hash->{SetFn} = "PID20_Set"; + $hash->{GetFn} = "PID20_Get"; + $hash->{NotifyFn} = "PID20_Notify"; + $hash->{AttrList} = + "pidActorValueDecPlaces:0,1,2,3,4,5 " + . "pidActorInterval " + . "pidActorTreshold " + . "pidActorErrorAction:freeze,errorPos " + . "pidActorErrorPos " + . "pidActorKeepAlive " + . "pidActorLimitLower " + . "pidActorLimitUpper " + . "pidCalcInterval " + . "pidDeltaTreshold " + . "pidDesiredName " + . "pidFactor_P " + . "pidFactor_I " + . "pidFactor_D " + . "pidMeasuredName " + . "pidSensorTimeout " + . "pidReverseAction " + . "pidUpdateInterval " +# . "pidDebugEnable:0,1 "; + . "pidDebugSensor:0,1 " + . "pidDebugActuation:0,1 " + . "pidDebugCalc:0,1 " + . "pidDebugDelta:0,1 " + . "pidDebugUpdate:0,1 " + . "pidDebugNotify:0,1 " + + . "disable:0,1 " + . $readingFnAttributes; + +} + + +######################################## +sub PID20_TimeDiff($) { + my ($strTS)=@_; + #my ( $package, $filename, $line ) = caller(0); + #print "PID $strTS line $line \n"; + + my $serTS = (defined($strTS) && $strTS ne "") ? time_str2num($strTS) : gettimeofday(); + my $timeDiff = gettimeofday()- $serTS; + $timeDiff=0 if ( $timeDiff<0); + return $timeDiff; +} + +######################################## +sub PID20_Define($$$) +{ + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + my $name = $a[0]; + my $reFloat ='^([\\+,\\-]?\\d+\\.?\d*$)'; # gleitpunkt + + if ( @a != 4) + { + return "wrong syntax: define PID20 " . ":reading:[regexp] [:cmd] "; + } + ################### + # Sensor + my ( $sensor, $reading, $regexp ) = split( ":", $a[2], 3 ); + + # if sensor unkonwn + if ( !$defs{$sensor} ) + { + my $msg = "$name: Unknown sensor device $sensor specified"; + PID20_Log $hash, 1, $msg; + return $msg; + } + + # if reading of sender is unkown + if (ReadingsVal($sensor,$reading,'unknown') eq 'unkown') + { + my $msg = "$name: Unknown reading $reading for sensor device $sensor specified"; + PID20_Log $hash, 1, $msg; + return $msg; + } + + $hash->{helper}{sensor} = $sensor; + + # defaults for regexp + if ( !$regexp ) + { + $regexp=$reFloat; + } + + $hash->{helper}{reading} = $reading; + $hash->{helper}{regexp} = $regexp; + + # Actor + my ( $actor, $cmd ) = split( ":", $a[3],2 ); + + if ( !$defs{$actor} ) + { + my $msg = "$name: Unknown actor device $actor specified"; + PID20_Log $hash, 1, $msg; + return $msg; + } + + $hash->{helper}{actor} = $actor; + $hash->{helper}{actorCommand}= (defined ($cmd)) ? $cmd :""; + $hash->{helper}{stopped}=0; + $hash->{helper}{adjust}=""; + + $modules{PID20}{defptr}{$name}=$hash; + + readingsSingleUpdate( $hash, 'state', 'initializing',1 ); + + RemoveInternalTimer($name); + InternalTimer( gettimeofday() + 10, "PID20_Calc", $name, 0 ); + return undef; +} +######################################## +sub PID20_Undef($$) +{ + my ( $hash, $arg ) = @_; + RemoveInternalTimer($hash->{NAME}); + return undef; +} +sub +######################################## +# we need a gradient for delta as base for d-portion calculation +# +PID20_Notify($$) +{ + my ($hash, $dev) = @_; + my $name = $hash->{NAME}; + my $sensorName = $hash->{helper}{sensor}; + + my $DEBUG = AttrVal($name, 'pidDebugNotify', '0' ) eq '1'; + + + # no action if disabled + if (defined($attr{$name}) && defined($attr{$name}{disable}) ) + { + $hash->{helper}{sensorTsOld}=undef; + return "" ; + } + + return if($dev->{NAME} ne $sensorName); + + my $sensorReadingName = $hash->{helper}{reading}; + my $regexp = $hash->{helper}{regexp}; + my $desiredName = AttrVal( $name, 'pidDesiredName', 'desired' ); + my $desired = ReadingsVal( $name,$desiredName, undef ); + + my $max = int(@{$dev->{CHANGED}}); + PID20_Log $hash, 4, "check $max readings for ". $sensorReadingName; + + for (my $i = 0; $i < $max; $i++) { + my $s = $dev->{CHANGED}[$i]; + + # continue, if no match with reading-name + $s = "" if(!defined($s)); + PID20_Log $hash, 5, "check event:<$s>"; + next if($s !~ m/$sensorReadingName/); + + # ---- build difference current - old value + # get sensor value + + my $sensorStr = ReadingsVal( $sensorName, $sensorReadingName, undef ); + $sensorStr =~ m/$regexp/; + my $sensorValue = $1; + + # calc difference of delta/deltaOld + my $delta = $desired - $sensorValue if (defined($desired)); + my $deltaOld = ($hash->{helper}{deltaOld}+0) if (defined($hash->{helper}{deltaOld})); + + my $deltaDiff = ($delta - $deltaOld) if (defined($delta) && defined($deltaOld)); + PID20_Log $hash, 5, "Diff: delta[".sprintf( "%.2f",$delta)."]" + ." - deltaOld[".sprintf( "%.2f",$deltaOld)."]" + ."= Diff[".sprintf( "%.2f",$deltaDiff)."]" + if ($DEBUG); + + # ----- build difference of timestamps (ok) + my $deltaOldTsStr = $hash->{helper}{deltaOldTS}; + my $deltaOldTsNum =time_str2num($deltaOldTsStr) if (defined($deltaOldTsStr)); + my $nowTsNum = gettimeofday(); + my $tsDiff = ($nowTsNum - $deltaOldTsNum) + if ( defined($deltaOldTsNum) && (($nowTsNum - $deltaOldTsNum)>0)); + PID20_Log $hash, 5, "tsDiff: tsDiff = $tsDiff " if ($DEBUG); + + # ----- calculate gradient of delta + my $deltaGradient =$deltaDiff/$tsDiff if(defined($deltaDiff) && defined($tsDiff) && ($tsDiff>0)); + $deltaGradient = 0 if (!defined($deltaGradient)); + + my $sdeltaDiff = ($deltaDiff)?sprintf( "%.2f",$deltaDiff):""; + my $sTSDiff = ($tsDiff)?sprintf( "%.2f",$tsDiff):""; + my $sDeltaGradient=($deltaGradient)?sprintf( "%.6f",$deltaGradient):""; + PID20_Log $hash, 5, "deltaGradient: (Diff[$sdeltaDiff]" + ."/tsDiff[$sTSDiff]" + ."=deltaGradient per sec [$sDeltaGradient]" if ($DEBUG); + + # ----- store results + $hash->{helper}{deltaGradient}=$deltaGradient; + $hash->{helper}{deltaOld}= $delta; + $hash->{helper}{deltaOldTS}= TimeNow(); + + last; + } + return ""; +} +######################################## +sub PID20_Get($@) +{ + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $usage = "Unknown argument $a[1], choose one of params:noArg"; + return $usage if ( @a < 2 ); + my $cmd = lc( $a[1] ); + given ($cmd) + { + when ('params') + { + my $ret = "Defined parameters for PID20 $name:\n\n"; + $ret .= 'Actor name : ' . $hash->{helper}{actor} . "\n"; + $ret .= 'Actor cmd : ' . $hash->{helper}{actorCommand} . "\n\n"; + $ret .= 'Sensor name : ' . $hash->{helper}{sensor} . "\n"; + $ret .= 'Sensor reading : ' . $hash->{helper}{reading} . "\n\n"; + $ret .= 'Sensor regexp : ' . $hash->{helper}{regexp} . "\n\n"; + $ret .= 'Factor P : ' . $hash->{helper}{factor_P} . "\n"; + $ret .= 'Factor I : ' . $hash->{helper}{factor_I} . "\n"; + $ret .= 'Factor D : ' . $hash->{helper}{factor_D} . "\n\n"; + $ret .= 'Actor lower limit: ' . $hash->{helper}{actorLimitLower} . "\n"; + $ret .= 'Actor upper limit: ' . $hash->{helper}{actorLimitUpper} . "\n"; + return $ret; + } + default { return $usage; }; + } +} +######################################## +sub PID20_Set($@) +{ + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $reFloat ='^([\\+,\\-]?\\d+\\.?\d*$)'; + + my $usage = + "Unknown argument $a[1], choose one of stop:noArg start:noArg restart " + . AttrVal( $name, 'pidDesiredName', 'desired' ); + return $usage if ( @a < 2 ); + + my $cmd = lc( $a[1] ); + my $desiredName = lc(AttrVal( $name, 'pidDesiredName', 'desired' )); + #PID20_Log $hash, 3, "name:$name cmd:$cmd $desired:$desired"; + + given ($cmd) + { + when ("?") + { + return $usage; + } + + when ( $desiredName ) + { + return "Set " . AttrVal( $name, 'pidDesiredName', 'desired' ) . " needs a parameter" + if ( @a != 3 ); + + my $value=$a[2]; + $value=($value=~ m/$reFloat/) ? $1:undef; + return "value ".$a[2]." is not a number" + if (!defined($value)); + + readingsSingleUpdate( $hash, $cmd, $value, 1 ); + PID20_Log $hash, 3, "set $name $cmd $a[2]"; + } + + when ("start") + { + return "Set start needs a parameter" + if ( @a != 2 ); + $hash->{helper}{stopped} =0; + + } + + when ("stop") + { + return "Set stop needs a parameter" + if ( @a != 2 ); + $hash->{helper}{stopped} =1; + PID20_Calc($hash); + } + + when ("restart") + { + return "Set restart needs a parameter" + if ( @a != 3 ); + + my $value=$a[2]; + $value=($value=~ m/$reFloat/) ? $1:undef; + #PID20_Log $hash, 1, "value:$value"; + + return "value ".$a[2]." is not a number" + if (!defined($value)); + + $hash->{helper}{stopped} =0; + $hash->{helper}{adjust} =$value; + PID20_Log $hash, 3, "set $name $cmd $value"; + } + + when ("calc") # inofficial function, only for debugging purposes + { + PID20_Calc($hash); + } + + default + { + return $usage; + } + } + return; +} + +######################################## +# disabled = 0 +# idle = 1 +# processing = 2 +# stopped = 3 +# alarm = 4 +sub PID20_Calc($) +{ + my $reUINT = '^([\\+]?\\d+)$'; # uint without whitespaces + my $re01 = '^([0,1])$'; # only 0,1 + my $reINT = '^([\\+,\\-]?\\d+$)'; # int + my $reFloatpos ='^([\\+]?\\d+\\.?\d*$)'; # gleitpunkt positiv + my $reFloat ='^([\\+,\\-]?\\d+\\.?\d*$)'; # gleitpunkt + + my ($name) = @_; + my $hash = $defs{$name}; + + my $sensor = $hash->{helper}{sensor}; + my $reading = $hash->{helper}{reading}; + my $regexp = $hash->{helper}{regexp}; + + my $DEBUG_Sensor = AttrVal($name, 'pidDebugSensor', '0' ) eq '1'; + my $DEBUG_Actuation = AttrVal($name, 'pidDebugActuation', '0' ) eq '1'; + my $DEBUG_Delta = AttrVal($name, 'pidDebugDelta', '0' ) eq '1'; + my $DEBUG_Calc = AttrVal($name, 'pidDebugCalc', '0' ) eq '1'; + my $DEBUG_Update = AttrVal($name, 'pidDebugUpdate', '0' ) eq '1'; + + my $DEBUG = $DEBUG_Sensor || $DEBUG_Actuation || $DEBUG_Calc || $DEBUG_Delta || $DEBUG_Update ; + + my $actuation = ""; + my $actuationDone = ReadingsVal( $name, 'actuation', "" ); + my $actuationCalc = ReadingsVal( $name, 'actuationCalc', "" ); + my $actuationCalcOld = $actuationCalc; + my $actorTimestamp = ($hash->{helper}{actorTimestamp}) + ?$hash->{helper}{actorTimestamp}:FmtDateTime(gettimeofday()-3600*24); + + my $sensorStr = ReadingsVal( $sensor, $reading, "" ); + my $sensorValue = ""; + my $sensorTS = ReadingsTimestamp( $sensor, $reading, undef ); + my $sensorIsAlive = 0; + + my $iPortion = ReadingsVal( $name, 'p_i', 0 ); + my $pPortion = ""; + my $dPortion = ""; + + my $stateStr = ""; + + my $deltaOld = ReadingsVal( $name, 'delta', 0 ); + my $delta = ""; + my $deltaGradient = ($hash->{helper}{deltaGradient})?$hash->{helper}{deltaGradient}:0; + + my $calcReq = 0; + + # ---------------- check different conditions + while (1) + { + # --------------- retrive values from attributes + + $hash->{helper}{actorInterval} = (AttrVal($name, 'pidActorInterval', 180 ) =~ m/$reUINT/) ? $1:180; + $hash->{helper}{actorThreshold} = (AttrVal($name, 'pidActorTreshold', 1 ) =~ m/$reUINT/) ? $1:1; + $hash->{helper}{actorKeepAlive} = (AttrVal($name, 'pidActorKeepAlive', 1800 ) =~ m/$reUINT/) ? $1:1800; + $hash->{helper}{actorValueDecPlaces} = (AttrVal($name, 'pidActorValueDecPlaces', 0 ) =~ m/$reUINT/) ? $1:0; + + $hash->{helper}{actorErrorAction} = (AttrVal($name, 'pidActorErrorAction', 'freeze') eq 'errorPos') ?'errorPos':'freeze'; + $hash->{helper}{actorErrorPos} = (AttrVal($name, 'pidActorErrorPos', 0 ) =~ m/$reINT/) ? $1:0; + + + $hash->{helper}{calcInterval} = (AttrVal($name, 'pidCalcInterval', 60 ) =~ m/$reUINT/) ? $1:60; + $hash->{helper}{deltaTreshold} = (AttrVal($name, 'pidDeltaTreshold', 0 ) =~ m/$reFloatpos/) ? $1:0; + $hash->{helper}{disable} = (AttrVal($name, 'Disable', 0 ) =~ m/$re01/) ? $1:''; + + $hash->{helper}{sensorTimeout} = (AttrVal($name, 'pidSensorTimeout', 3600 ) =~ m/$reUINT/) ? $1:3600; + $hash->{helper}{reverseAction} = (AttrVal($name, 'pidReverseAction', 0 ) =~ m/$re01/) ? $1:0; + $hash->{helper}{updateInterval} = (AttrVal($name, 'pidUpdateInterval', 600 ) =~ m/$reUINT/) ? $1:600; + + $hash->{helper}{measuredName} = AttrVal($name, 'pidMeasuredName', 'measured') ; + $hash->{helper}{desiredName} = AttrVal($name, 'pidDesiredName', 'desired') ; + + $hash->{helper}{actorLimitLower} = (AttrVal($name, 'pidActorLimitLower', 0) =~ m/$reFloat/) ? $1:0; + my $actorLimitLower = $hash->{helper}{actorLimitLower}; + + $hash->{helper}{actorLimitUpper} = (AttrVal($name, 'pidActorLimitUpper', 100) =~ m/$reFloat/) ? $1:100; + my $actorLimitUpper = $hash->{helper}{actorLimitUpper}; + + $hash->{helper}{factor_P} = (AttrVal($name, 'pidFactor_P', 25) =~ m/$reFloatpos/) ? $1:25; + $hash->{helper}{factor_I} = (AttrVal($name, 'pidFactor_I', 0.25) =~ m/$reFloatpos/) ? $1:0.25; + $hash->{helper}{factor_D} = (AttrVal($name, 'pidFactor_D', 0) =~ m/$reFloatpos/) ? $1:0; + + if ($hash->{helper}{disable}) + { + $stateStr="disabled"; + last; + } + + if ($hash->{helper}{stopped}) + { + $stateStr="stopped"; + last; + } + + my $desired = ReadingsVal( $name,$hash->{helper}{desiredName}, "" ); + + # sensor found + PID20_Log $hash, 2, "--------------------------" if ($DEBUG); + PID20_Log $hash, 2, "S1 sensorStr:$sensorStr sensorTS:$sensorTS" if ($DEBUG_Sensor); + $stateStr="alarm - no $reading yet for $sensor" if ( !$sensorStr && !$stateStr); + + # sensor alive + if ($sensorStr && $sensorTS) + { + my $timeDiff = PID20_TimeDiff($sensorTS); + $sensorIsAlive = 1 if ( $timeDiff <= $hash->{helper}{sensorTimeout} ); + $sensorStr =~ m/$regexp/; + $sensorValue = $1; + $sensorValue="" if (!defined($sensorValue)); + PID20_Log $hash, 2, "S2 timeOfDay:".gettimeofday() + ." timeDiff:$timeDiff sensorTimeout:".$hash->{helper}{sensorTimeout} + ." --> sensorIsAlive:$sensorIsAlive" if ($DEBUG_Sensor); + } + + # sensor dead + $stateStr="alarm - dead sensor" if (!$sensorIsAlive && !$stateStr); + + # missing desired + $stateStr="alarm - missing desired" if ($desired eq "" && !$stateStr); + + # check delta threshold + $delta =($desired ne "" && $sensorValue ne "" ) ? $desired - $sensorValue : ""; + + $calcReq = 1 if (!$stateStr && $delta ne "" && (abs($delta) >= abs( $hash->{helper}{deltaTreshold})) ); + + PID20_Log $hash, 2, "D1 desired[". ($desired ne "") ? sprintf( "%.1f", $desired) : "" + ."] - sensorValue: [". ($sensorValue ne "") ? sprintf( "%.1f", $sensorValue) : "" + ."] = delta[". ($delta ne "") ? sprintf( "%.2f", $delta):"" + ."] calcReq:$calcReq" + if ($DEBUG_Delta); + + #request for calculation + + # ---------------- calculation request + if ($calcReq) + { + # reverse action requested + my $workDelta = ( $hash->{helper}{reverseAction} ==1 ) ? -$delta: $delta; + my $deltaOld = - $deltaOld if ($hash->{helper}{reverseAction} ==1 ); + + # calc p-portion + $pPortion = $workDelta * $hash->{helper}{factor_P}; + + # calc d-Portion + $dPortion = ( $deltaGradient ) * $hash->{helper}{calcInterval} * $hash->{helper}{factor_D}; + + + # calc i-portion respecting windUp + # freeze i-portion if windUp is active + my $isWindup = + $actuationCalcOld && + ( + ( $workDelta > 0 && $actuationCalcOld > $actorLimitUpper ) + || ( $workDelta < 0 && $actuationCalcOld < $actorLimitLower ) + ); + + if ($hash->{helper}{adjust} ne "") + { + $iPortion = $hash->{helper}{adjust} - ($pPortion + $dPortion); + $iPortion= $actorLimitUpper if($iPortion > $actorLimitUpper); + $iPortion= $actorLimitLower if($iPortion < $actorLimitLower); + PID20_Log $hash, 5, "adjust request with:".$hash->{helper}{adjust}." ==> p_i:$iPortion"; + + $hash->{helper}{adjust}=""; + } + elsif ( !$isWindup ) # integrate only if no windUp + { + # normalize the intervall to minute=60 seconds + $iPortion = $iPortion + $workDelta * $hash->{helper}{factor_I}*$hash->{helper}{calcInterval}/60; + $hash->{helper}{isWindUP} = 0; + } + $hash->{helper}{isWindUP} = $isWindup; + + # calc actuation + $actuationCalc = $pPortion + $iPortion + $dPortion; + PID20_Log $hash, 2, "P1 delta:".sprintf( "%.2f",$delta) + ." isWindup:$isWindup" + if ($DEBUG_Calc); + + PID20_Log $hash, 2, "P2 pPortion:".sprintf( "%.2f",$pPortion) + ." iPortion:".sprintf( "%.2f",$iPortion) + ." dPortion:".sprintf( "%.2f",$dPortion) + ." actuationCalc:".sprintf( "%.2f", $actuationCalc) if ($DEBUG_Calc); + + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, 'p_p', $pPortion ); + readingsBulkUpdate( $hash, 'p_i', $iPortion ); + readingsBulkUpdate( $hash, 'p_d', $dPortion ); + readingsBulkUpdate( $hash, 'actuationCalc', $actuationCalc ); + readingsBulkUpdate( $hash, 'delta', $delta ); + readingsEndUpdate( $hash, 0 ); + #PID20_Log $hash, 3, "calculation done"; + } + + # ---------------- acutation request + my $noTrouble = ($desired ne "" && $sensorIsAlive); + + # check actor fallback in case of sensor fault + if (!$sensorIsAlive && ($hash->{helper}{actorErrorAction} eq "errorPos")) + { + $stateStr .= "- force pid-output to errorPos"; + $actuationCalc=$hash->{helper}{actorErrorPos}; + $actuationCalc="" if (!defined($actuationCalc)); + } + + # check acutation diff + $actuation = $actuationCalc; + + # limit $actuation + $actuation= $actorLimitUpper if($actuation ne "" && ($actuation > $actorLimitUpper)); + $actuation= $actorLimitLower if($actuation ne "" && ($actuation < $actorLimitLower)); + + # check if round request + my $fmt= "%.".$hash->{helper}{actorValueDecPlaces}."f"; + $actuation = sprintf( $fmt, $actuation) if ($actuation ne ""); + + my $actuationDiff = abs( $actuation - $actuationDone) if ($actuation ne "" && $actuationDone ne ""); + PID20_Log $hash, 2, "A1 act:$actuation actDone:$actuationDone " + ." actThreshold:".$hash->{helper}{actorThreshold} + ." actDiff:$actuationDiff" + if ($DEBUG_Actuation); + + # check threshold-condition for actuation + my $rsTS = $actuationDone ne "" # limit exceeded + && $actuationDiff >= $hash->{helper}{actorThreshold}; + + my $rsUp = $actuationDone ne "" # upper range + && $actuation>$actorLimitUpper-$hash->{helper}{actorThreshold} + && $actuationDiff != 0 + && $actuation >=$actorLimitUpper; + + my $rsDown = $actuationDone ne "" # low range + && $actuation<$actorLimitLower+$hash->{helper}{actorThreshold} + && $actuationDiff != 0 + && $actuation <=$actorLimitLower; + + my $rsLimit = $actuationDone ne "" + && ($actuationDone<$actorLimitLower || $actuationDone>$actorLimitUpper); + + my $actuationByThreshold = ( ($rsTS || $rsUp || $rsDown ) && $noTrouble); + + PID20_Log $hash, 2, "A2 rsTS:$rsTS rsUp:$rsUp rsDown:$rsDown noTrouble:$noTrouble" if ($DEBUG_Actuation); + + # check time condition for actuation + my $actTimeDiff = PID20_TimeDiff($actorTimestamp); # $actorTimestamp is valid in each case + my $actuationByTime = ($noTrouble) && ($actTimeDiff > $hash->{helper}{actorInterval}); + + PID20_Log $hash, 2, "A3 actTS:$actorTimestamp" + ." actTimeDiff:".sprintf( "%.2f",$actTimeDiff) + ." actInterval:".$hash->{helper}{actorInterval} + ."-->actByTime:$actuationByTime " if ($DEBUG_Actuation); + + # check keep alive condition for actuation + my $actuationKeepAliveReq = ($actTimeDiff >= $hash->{helper}{actorKeepAlive}) + if (defined($actTimeDiff) && $actuation ne ""); + + # summary actuation reques + + my $actuationReq = ( + ($actuationByThreshold && $actuationByTime) + || $actuationKeepAliveReq + || $rsLimit + || $actuationDone eq "" # startup condition + ) + && $actuation ne ""; + + PID20_Log $hash, 2, "A4 (actByTh:$actuationByThreshold && actByTime:$actuationByTime)" + ."||actKeepAlive:$actuationKeepAliveReq" + ."||rsLimit:$rsLimit=actnReq:$actuationReq" if ($DEBUG_Actuation); + + # perform output to actor + if ($actuationReq) + { + #build command for fhem + PID20_Log $hash, 5, "actor:".$hash->{helper}{actor} + ." actorCommand:".$hash->{helper}{actorCommand} + ." actuation:".$actuation; + + my $cmd= sprintf("set %s %s %g", $hash->{helper}{actor}, $hash->{helper}{actorCommand},$actuation); + + # execute command + my $ret; + $ret = fhem $cmd; + + # note timestamp + $hash->{helper}{actorTimestamp}=TimeNow(); + $actuationDone=$actuation; + my $retStr="" if (!$ret); + PID20_Log $hash, 3, "<$cmd> with ret:$retStr"; + } + + my $updateAlive= ($actuation ne "") + && PID20_TimeDiff(ReadingsTimestamp( $name, 'actuation', gettimeofday()))>=$hash->{helper}{updateInterval}; + + my $updateReq=(($actuationReq || $updateAlive) && $actuation ne ""); + + PID20_Log $hash, 2, "U1 actReq:$actuationReq updateAlive:$updateAlive --> updateReq:$updateReq" + if ($DEBUG_Update); + + # ---------------- update request + if ($updateReq) + { + readingsBeginUpdate($hash); + readingsBulkUpdate( $hash, $hash->{helper}{desiredName}, $desired ) if ($desired ne ""); + readingsBulkUpdate( $hash, $hash->{helper}{measuredName}, $sensorValue ) if ($sensorValue ne ""); + readingsBulkUpdate( $hash, 'p_p', $pPortion ) if ($pPortion ne""); + readingsBulkUpdate( $hash, 'p_d', $dPortion ) if ($dPortion ne ""); + readingsBulkUpdate( $hash, 'p_i', $iPortion ) if ($iPortion ne ""); + readingsBulkUpdate( $hash, 'actuation', $actuationDone ) if ($actuationDone ne ""); + readingsBulkUpdate( $hash, 'actuationCalc', $actuationCalc ) if ($actuationCalc ne ""); + readingsBulkUpdate( $hash, 'delta', $delta ) if ($delta ne ""); + readingsEndUpdate( $hash, 1 ); + PID20_Log $hash, 5, "readings updated"; + } + last; + } # end while + + # update statePID. + $stateStr = "idle" if (!$stateStr && !$calcReq); + $stateStr = "processing" if (!$stateStr && $calcReq); + readingsSingleUpdate( $hash, 'state', $stateStr , 0 ); + + PID20_Log $hash, 2, "C1 stateStr:$stateStr calcReq:$calcReq" if ($DEBUG_Calc); + + # timer setup + my $next = gettimeofday() + $hash->{helper}{calcInterval}; + RemoveInternalTimer($name); # prevent multiple timers for same hash + InternalTimer( $next, "PID20_Calc", $name, 1 ); + + #PID20_Log $hash, 2, "InternalTimer next:".FmtDateTime($next)." PID20_Calc name:$name DEBUG_Calc:$DEBUG_Calc"; + + return; +} + + +1; + +=pod +=begin html + + +

PID20

+
    + + + Define +
      +
      + define <name> PID20 <sensor[:reading[:regexp]]> <actor:cmd > +

      + This module provides a PID device, using <sensor> and <actor>
      +
    +

    + + + Set-Commands
    +
      + +
      + set <name> desired <value> +

      +
        Set desired value for PID
      +
      + +
      + set <name> start +

      +
        Start PID processing again, using frozen values from former stop.
      +
      + +
      + set <name> stop +

      +
        PID stops processing, freezing all values.
      +
      + +
      + set <name> restart <value> +

      +
        Same as start, but uses value as start value for actor
      +
      + +
    +

    + + + Get-Commands
    +
      + +
      + get <name> params +

      +
        Get list containing current parameters.
      +
      + +
    +

    + + + Attributes

    +
      +
    • readingFnAttributes
    • +
      +
    • disable - disable the PID device, possible values: 0,1; default: 0
    • +
    • pidActorValueDecPlaces - number of demicals, possible values: 0..5; default: 0
    • +
    • pidActorInterval - number of seconds to wait between to commands sent to actor; default: 180
    • +
    • pidActorTreshold - threshold to be reached before command will be sent to actor; default: 1
    • +
    • pidActorErrorAction - required action on error, possible values: freeze,errorPos; default: freeze
    • +
    • pidActorErrorPos - actor's position to be used in case of error; default: 0
    • +
    • pidActorKeepAlive - number of seconds to force command to be sent to actor; default: 1800
    • +
    • pidActorLimitLower - lower limit for actor; default: 0
    • +
    • pidActorLimitUpper - upper limit for actor; default: 100
    • +
    • pidCalcInterval - interval (seconds) to calculate new pid values; default: 60
    • +
    • pidDeltaTreshold - if delta < delta-threshold the pid will enter idle state; default: 0
    • +
    • pidDesiredName - reading's name for desired value; default: desired
    • +
    • pidFactor_P - P value for PID; default: 25
    • +
    • pidFactor_I - I value for PID; default: 0.25
    • +
    • pidFactor_D - D value for PID; default: 0
    • +
    • pidMeasuredName - reading's name for measured value; default: measured
    • +
    • pidSensorTimeout - number of seconds to wait before sensor will be recognized n/a; default: 3600
    • +
    • pidReverseAction - reverse PID operation mode, possible values: 0,1; default: 0
    • +
    • pidUpdateInterval - number of seconds to wait before an update will be forced for plotting; default: 300
    • + +
    +

    + + Generated Readings/Events: +

    +
      +
    • actuation - real actuation set to actor
    • +
    • actuationCalc - internal actuation calculated without limits
    • +
    • delta - current difference desired - measured
    • +
    • desired - desired value
    • +
    • measured - measured value
    • +
    • p_p - p value of pid calculation
    • +
    • p_i - i value of pid calculation
    • +
    • p_d - d value of pid calculation
    • +
    • state - current device state
    • +
      + Names for desired and measured readings can be changed by corresponding attributes (see above).
      +
    +

    + + Additional informations

    + + +
+ +=end html diff --git a/HISTORY b/HISTORY index 0248aa466..99411b248 100644 --- a/HISTORY +++ b/HISTORY @@ -557,4 +557,7 @@ - Mon Mar 24 2014 (betateilchen) - old module 98_PID.pm moved to contrib - will be replaced by 98_PID20.pm in next major release \ No newline at end of file + will be replaced by 98_PID20.pm in next major release + +- Wed Mar 26 2014 (John / betateilchen) + - added new module 98_PID20.pm as announced replacement for old 98_PID.pm \ No newline at end of file diff --git a/MAINTAINER.txt b/MAINTAINER.txt index 08b5d8749..c9b639309 100644 --- a/MAINTAINER.txt +++ b/MAINTAINER.txt @@ -199,6 +199,7 @@ FHEM/98_Heating_Control.pm dietmar63 http://forum.fhem.de Unterstue FHEM/98_HTTPMOD.pm stefanstrobel http://forum.fhem.de Sonstiges FHEM/98_JsonList.pm mfr69bs http://forum.fhem.de Automatisierung FHEM/98_JsonList2.pm rudolfkoenig http://forum.fhem.de Automatisierung +FHEM/98_PID20.pm John http://forum.fhem.de Automatisierung FHEM/98_RandomTimer.pm dietmar63 http://forum.fhem.de Unterstuetzende Dienste FHEM/98_SVG.pm rudolfkoenig http://forum.fhem.de Frontends FHEM/98_THRESHOLD.pm damian-s http://forum.fhem.de Automatisierung