############################################## # $Id: 98_PID.pm 4930 2014-02-15 03:59:16Z betateilchen $ # This module is derived from the contrib/99_PID by Alexander Titzel. package main; use strict; use warnings; sub PID_sv($$$); sub PID_setValue($); ########################## sub PID_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "PID_Define"; $hash->{SetFn} = "PID_Set"; $hash->{NotifyFn} = "PID_Notify"; $hash->{AttrList} = "disable:0,1 roundValveValue:0,1"; } ########################## sub PID_Define($$$) { my ($pid, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $pn = $a[0]; if(@a < 4 || @a > 7) { return "wrong syntax: define PID " . "[:reading:regexp] [:cmd:min:max] [p i d]"; } ################### # Sensor my ($sensor, $reading, $regexp) = split(":", $a[2], 3); if(!$defs{$sensor}) { my $msg = "$pn: Unknown sensor device $sensor specified"; Log3 $pn, 2, $msg; return $msg; } $pid->{sensor} = $sensor; if(!$regexp) { my $t = $defs{$sensor}{TYPE}; if($t eq "HMS" || $t eq "CUL_WS" || $t eq "CUL_HM" ) { $reading = "temperature"; $regexp = '([\\d\\.]*)'; } else { my $msg = "$pn: Unknown sensor type $t, specify regexp"; Log3 $pn, 2, $msg; return $msg; } } $pid->{reading} = $reading; $pid->{regexp} = $regexp; ################### # Actor my ($actor, $cmd, $min, $max) = split(":", $a[3], 4); my ($p_p, $p_i, $p_d) = (0, 0, 0); if(!$defs{$actor}) { my $msg = "$pn: Unknown actor device $actor specified"; Log3 $pn, 2, $msg; return $msg; } $pid->{actor} = $actor; if(!$max) { my $t = $defs{$actor}{TYPE}; if($t eq "FHT8V") { $cmd = "valve"; $min = 0; $max = 100; $p_p = 65.0/2.55; $p_i = 7.8/2.55; $p_d = 15.0/2.55; } else { my $msg = "$pn: Unknown actor type $t, specify command:min:max"; Log3 $pn, 2, $msg; return $msg; } } $pid->{command} = $cmd; $pid->{pFactor} = (@a > 4 ? $a[4] : $p_p); $pid->{iFactor} = (@a > 5 ? $a[5] : $p_i); $pid->{dFactor} = (@a > 6 ? $a[6] : $p_d); $pid->{satMin} = $min; $pid->{satMax} = $max; PID_sv($pid, 'delta', 0.0); PID_sv($pid, 'actuation', 0.0); PID_sv($pid, 'integrator', 0.0); $pid->{STATE} = 'initialized'; return undef; } ########################## sub PID_Set($@) { my ($pid, @a) = @_; my $pn = $pid->{NAME}; return "" if($attr{$pn} && $attr{$pn}{disable}); return "Need a parameter for set" if(@a < 2); my $arg = $a[1]; if($arg eq "factors" ) { return "Set factors needs 3 parameters (p i d)" if(@a != 5); $pid->{pFactor} = $a[2]; $pid->{iFactor} = $a[3]; $pid->{dFactor} = $a[4]; # modify DEF, alse save won't work. my @d = split(' ', $pid->{DEF}); $pid->{DEF} = "$d[0] $d[1] $a[2] $a[3] $a[4]"; } elsif ($arg eq "desired" ) { return "Set desired needs a numeric parameter" if(@a != 3 || $a[2] !~ m/^[\d\.]*$/); Log3 $pn, 3, "PID set $pn $arg $a[2]"; PID_sv($pid, 'desired', $a[2]); PID_setValue($pid); } else { return "Unknown argument $a[1], choose one of factors desired" } return ""; } ########################## sub PID_Notify($$) { my ($pid, $dev) = @_; my $pn = $pid->{NAME}; return "" if($attr{$pn} && $attr{$pn}{disable}); return if($dev->{NAME} ne $pid->{sensor}); my $reading = $pid->{reading}; my $max = int(@{$dev->{CHANGED}}); for (my $i = 0; $i < $max; $i++) { my $s = $dev->{CHANGED}[$i]; $s = "" if(!defined($s)); next if($s !~ m/$reading/); PID_setValue($pid); last; } return ""; } ########################## sub PID_saturate($$) { my ($pid, $v) = @_; return $pid->{satMax} if($v > $pid->{satMax}); return $pid->{satMin} if($v < $pid->{satMin}); return $v; } sub PID_sv($$$) { my ($pid,$name,$val) = @_; $pid->{READINGS}{$name}{VAL} = $val; $pid->{READINGS}{$name}{TIME} = TimeNow(); } sub PID_gv($$) { my ($pid,$name) = @_; return $pid->{READINGS}{$name}{VAL} if($pid->{READINGS} && $pid->{READINGS}{$name}); return undef; } sub PID_setValue($) { my ($pid) = @_; my $pn = $pid->{NAME}; my $sensor = $pid->{sensor}; my $reading = $pid->{reading}; my $re = $pid->{regexp}; # Get the value from the READING my $inStr; $inStr = $defs{$sensor}{READINGS}{$reading}{VAL} if($defs{$sensor}{READINGS} && $defs{$sensor}{READINGS}{$reading}); if(!$inStr) { Log3 $pn, 4, "PID $pn: no $reading yet for $sensor"; return; } $inStr =~ m/$re/; my $in = $1; my $desired = PID_gv($pid, 'desired'); return if(!defined($desired)); my $delta = $desired - $in; my $p = $delta * $pid->{pFactor}; my $i = PID_saturate($pid, PID_gv($pid,'integrator')+$delta*$pid->{iFactor}); PID_sv($pid, 'integrator', $i); my $d = ($delta - PID_gv($pid,'delta')) * $pid->{dFactor}; PID_sv($pid, 'delta', $delta); my $a = PID_saturate($pid, $p + $i + $d); PID_sv($pid, 'actuation', $a); Log3 $pn, 4, sprintf("PID $pn: p:%.2f i:%.2f d:%.2f", $p, $i, $d); # Hack to round. my ($satMin, $satMax) = ($pid->{satMin}, $pid->{satMax}); $a = int($a) if(AttrVal($pn, "roundValveValue", ($satMax-$satMin >= 100))); my $ret = fhem sprintf("set %s %s %g", $pid->{actor}, $pid->{command}, $a); Log3 $pn, 1, "output of $pn command: $ret" if($ret); $pid->{STATE} = "$in (delta $delta)"; } 1; =pod not to be translated =begin html

PID

    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.

    Define
      define <name> PID sensor[:reading:regexp] actor[:cmd:min:max] [p i d]

      sensor[:reading:regexp] specifies the sensor, which is an already defined fhem device, e.g. a S300TH temperature sensor. The reading and regexp fields are necessary only for unknown devices (currently CUL_WS and HMS devices are "known"). Reading specifies the READINGS field of the sensor, and the regexp extracts the number from this field. E.g. for the complete definition for a CUL_WS device is: s300th_dev:temperature:([\d\.]*)

      actor[:cmd:min:max] specifies the actor, which is an already defined fhem device, e.g. an FHT8V valve. The cmd, min and max fields are necessary only for unknown devices (currently FHT8V is "known"). cmd specifies the command name for the actor, min the minimum value and max the maximum value. The complete definition for an FHT8V device is:fht8v_dev:valve:0:100

      p, i and d are the parameters use to controlling, see also the this wikipedia entry. The default values are around 25.5, 3 and 5.88, you probably need to tune these values. They can be also changed later.

      Examples:
        define wz_pid PID wz_th wz_fht8v

    Set
    • set <name> factors p i d
      Set the p, i and d factors, as described above.
    • set <name> desired <value>
      Set the desired value (e.g. temperature). Note: until this value is not set, no command is issued.

    Get
      N/A

    Attributes
    • disable
    • roundValveValue
      round the valve value to an integer, of the attribute is set to 1. The valve value is automatically rounded, if the attribtue is not set, and the difference between min and max is greater than 100.


=end html =cut