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