############################################## # $Id$ # 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 loglevel:0,1,2,3,4,5,6"; } ########################## sub PID_Define($$$) { my ($pid, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $n = $a[0]; if(@a < 4 || @a > 7) { my $msg = "wrong syntax: define PID " . "[:reading:regexp] [:cmd:min:max] [p i d]"; Log 2, $msg; return $msg; } ################### # Sensor my ($sensor, $reading, $regexp) = split(":", $a[2], 3); if(!$defs{$sensor}) { my $msg = "$n: Unknown sensor device $sensor specified"; Log 2, $msg; return $msg; } $pid->{sensor} = $sensor; if(!$regexp) { my $t = $defs{$sensor}{TYPE}; if($t eq "HMS" || $t eq "CUL_WS") { $reading = "temperature"; $regexp = '([\\d\\.]*)'; } else { my $msg = "$n: Unknown sensor type $t, specify regexp"; Log 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 = "$n: Unknown actor device $actor specified"; Log 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 = "$n: Unknown actor type $t, specify command:min:max"; Log 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 $n = $pid->{NAME}; 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\.]*$/); Log GetLogLevel($n,3), "PID set $n $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 $n = $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) { Log GetLogLevel($n,4), "PID $n: 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); Log GetLogLevel($n,4), sprintf("PID $n: p:%.2f i:%.2f d:%.2f", $p, $i, $d); # Hack to round. $a = int($a) if(($pid->{satMax} - $pid->{satMin}) >= 100); my $ret = fhem sprintf("set %s %s %g", $pid->{actor}, $pid->{command}, $a); Log GetLogLevel($n,1), "output of $n command: $ret" if($ret); $pid->{STATE} = "$in (delta $delta)"; } 1;