98_PID.pm 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. ##############################################
  2. # $Id: 98_PID.pm 4930 2014-02-15 03:59:16Z betateilchen $
  3. # This module is derived from the contrib/99_PID by Alexander Titzel.
  4. package main;
  5. use strict;
  6. use warnings;
  7. sub PID_sv($$$);
  8. sub PID_setValue($);
  9. ##########################
  10. sub
  11. PID_Initialize($)
  12. {
  13. my ($hash) = @_;
  14. $hash->{DefFn} = "PID_Define";
  15. $hash->{SetFn} = "PID_Set";
  16. $hash->{NotifyFn} = "PID_Notify";
  17. $hash->{AttrList} = "disable:0,1 roundValveValue:0,1";
  18. }
  19. ##########################
  20. sub
  21. PID_Define($$$)
  22. {
  23. my ($pid, $def) = @_;
  24. my @a = split("[ \t][ \t]*", $def);
  25. my $pn = $a[0];
  26. if(@a < 4 || @a > 7) {
  27. return "wrong syntax: define <name> PID " .
  28. "<sensor>[:reading:regexp] <actor>[:cmd:min:max] [p i d]";
  29. }
  30. ###################
  31. # Sensor
  32. my ($sensor, $reading, $regexp) = split(":", $a[2], 3);
  33. if(!$defs{$sensor}) {
  34. my $msg = "$pn: Unknown sensor device $sensor specified";
  35. Log3 $pn, 2, $msg;
  36. return $msg;
  37. }
  38. $pid->{sensor} = $sensor;
  39. if(!$regexp) {
  40. my $t = $defs{$sensor}{TYPE};
  41. if($t eq "HMS" || $t eq "CUL_WS" || $t eq "CUL_HM" ) {
  42. $reading = "temperature";
  43. $regexp = '([\\d\\.]*)';
  44. } else {
  45. my $msg = "$pn: Unknown sensor type $t, specify regexp";
  46. Log3 $pn, 2, $msg;
  47. return $msg;
  48. }
  49. }
  50. $pid->{reading} = $reading;
  51. $pid->{regexp} = $regexp;
  52. ###################
  53. # Actor
  54. my ($actor, $cmd, $min, $max) = split(":", $a[3], 4);
  55. my ($p_p, $p_i, $p_d) = (0, 0, 0);
  56. if(!$defs{$actor}) {
  57. my $msg = "$pn: Unknown actor device $actor specified";
  58. Log3 $pn, 2, $msg;
  59. return $msg;
  60. }
  61. $pid->{actor} = $actor;
  62. if(!$max) {
  63. my $t = $defs{$actor}{TYPE};
  64. if($t eq "FHT8V") {
  65. $cmd = "valve";
  66. $min = 0;
  67. $max = 100;
  68. $p_p = 65.0/2.55;
  69. $p_i = 7.8/2.55;
  70. $p_d = 15.0/2.55;
  71. } else {
  72. my $msg = "$pn: Unknown actor type $t, specify command:min:max";
  73. Log3 $pn, 2, $msg;
  74. return $msg;
  75. }
  76. }
  77. $pid->{command} = $cmd;
  78. $pid->{pFactor} = (@a > 4 ? $a[4] : $p_p);
  79. $pid->{iFactor} = (@a > 5 ? $a[5] : $p_i);
  80. $pid->{dFactor} = (@a > 6 ? $a[6] : $p_d);
  81. $pid->{satMin} = $min;
  82. $pid->{satMax} = $max;
  83. PID_sv($pid, 'delta', 0.0);
  84. PID_sv($pid, 'actuation', 0.0);
  85. PID_sv($pid, 'integrator', 0.0);
  86. $pid->{STATE} = 'initialized';
  87. return undef;
  88. }
  89. ##########################
  90. sub
  91. PID_Set($@)
  92. {
  93. my ($pid, @a) = @_;
  94. my $pn = $pid->{NAME};
  95. return "" if($attr{$pn} && $attr{$pn}{disable});
  96. return "Need a parameter for set" if(@a < 2);
  97. my $arg = $a[1];
  98. if($arg eq "factors" ) {
  99. return "Set factors needs 3 parameters (p i d)" if(@a != 5);
  100. $pid->{pFactor} = $a[2];
  101. $pid->{iFactor} = $a[3];
  102. $pid->{dFactor} = $a[4];
  103. # modify DEF, alse save won't work.
  104. my @d = split(' ', $pid->{DEF});
  105. $pid->{DEF} = "$d[0] $d[1] $a[2] $a[3] $a[4]";
  106. } elsif ($arg eq "desired" ) {
  107. return "Set desired needs a numeric parameter"
  108. if(@a != 3 || $a[2] !~ m/^[\d\.]*$/);
  109. Log3 $pn, 3, "PID set $pn $arg $a[2]";
  110. PID_sv($pid, 'desired', $a[2]);
  111. PID_setValue($pid);
  112. } else {
  113. return "Unknown argument $a[1], choose one of factors desired"
  114. }
  115. return "";
  116. }
  117. ##########################
  118. sub
  119. PID_Notify($$)
  120. {
  121. my ($pid, $dev) = @_;
  122. my $pn = $pid->{NAME};
  123. return "" if($attr{$pn} && $attr{$pn}{disable});
  124. return if($dev->{NAME} ne $pid->{sensor});
  125. my $reading = $pid->{reading};
  126. my $max = int(@{$dev->{CHANGED}});
  127. for (my $i = 0; $i < $max; $i++) {
  128. my $s = $dev->{CHANGED}[$i];
  129. $s = "" if(!defined($s));
  130. next if($s !~ m/$reading/);
  131. PID_setValue($pid);
  132. last;
  133. }
  134. return "";
  135. }
  136. ##########################
  137. sub
  138. PID_saturate($$)
  139. {
  140. my ($pid, $v) = @_;
  141. return $pid->{satMax} if($v > $pid->{satMax});
  142. return $pid->{satMin} if($v < $pid->{satMin});
  143. return $v;
  144. }
  145. sub
  146. PID_sv($$$)
  147. {
  148. my ($pid,$name,$val) = @_;
  149. $pid->{READINGS}{$name}{VAL} = $val;
  150. $pid->{READINGS}{$name}{TIME} = TimeNow();
  151. }
  152. sub
  153. PID_gv($$)
  154. {
  155. my ($pid,$name) = @_;
  156. return $pid->{READINGS}{$name}{VAL}
  157. if($pid->{READINGS} && $pid->{READINGS}{$name});
  158. return undef;
  159. }
  160. sub
  161. PID_setValue($)
  162. {
  163. my ($pid) = @_;
  164. my $pn = $pid->{NAME};
  165. my $sensor = $pid->{sensor};
  166. my $reading = $pid->{reading};
  167. my $re = $pid->{regexp};
  168. # Get the value from the READING
  169. my $inStr;
  170. $inStr = $defs{$sensor}{READINGS}{$reading}{VAL}
  171. if($defs{$sensor}{READINGS} && $defs{$sensor}{READINGS}{$reading});
  172. if(!$inStr) {
  173. Log3 $pn, 4, "PID $pn: no $reading yet for $sensor";
  174. return;
  175. }
  176. $inStr =~ m/$re/;
  177. my $in = $1;
  178. my $desired = PID_gv($pid, 'desired');
  179. return if(!defined($desired));
  180. my $delta = $desired - $in;
  181. my $p = $delta * $pid->{pFactor};
  182. my $i = PID_saturate($pid, PID_gv($pid,'integrator')+$delta*$pid->{iFactor});
  183. PID_sv($pid, 'integrator', $i);
  184. my $d = ($delta - PID_gv($pid,'delta')) * $pid->{dFactor};
  185. PID_sv($pid, 'delta', $delta);
  186. my $a = PID_saturate($pid, $p + $i + $d);
  187. PID_sv($pid, 'actuation', $a);
  188. Log3 $pn, 4, sprintf("PID $pn: p:%.2f i:%.2f d:%.2f", $p, $i, $d);
  189. # Hack to round.
  190. my ($satMin, $satMax) = ($pid->{satMin}, $pid->{satMax});
  191. $a = int($a) if(AttrVal($pn, "roundValveValue", ($satMax-$satMin >= 100)));
  192. my $ret = fhem sprintf("set %s %s %g", $pid->{actor}, $pid->{command}, $a);
  193. Log3 $pn, 1, "output of $pn command: $ret" if($ret);
  194. $pid->{STATE} = "$in (delta $delta)";
  195. }
  196. 1;
  197. =pod
  198. not to be translated
  199. =begin html
  200. <a name="PID"></a>
  201. <h3>PID</h3>
  202. <ul>
  203. The PID device is a loop controller, used to set the value e.g of a heating
  204. valve dependent of the current and desired temperature.
  205. <br>
  206. <br>
  207. <a name="PIDdefine"></a>
  208. <b>Define</b>
  209. <ul>
  210. <code>define &lt;name&gt; PID sensor[:reading:regexp] actor[:cmd:min:max] [p i d]</code>
  211. <br><br>
  212. <code>sensor[:reading:regexp]</code> specifies the sensor, which is an
  213. already defined fhem device, e.g. a S300TH temperature sensor. The reading
  214. and regexp fields are necessary only for unknown devices (currently <a
  215. href="#CUL_WS">CUL_WS</a> and <a href="#HMS">HMS</a> devices are "known").
  216. Reading specifies the READINGS field of the sensor, and the regexp extracts
  217. the number from this field. E.g. for the complete definition for a CUL_WS
  218. device is: <code>s300th_dev:temperature:([\d\.]*)</code>
  219. <br><br>
  220. <code>actor[:cmd:min:max]</code> specifies the actor, which is an
  221. already defined fhem device, e.g. an FHT8V valve. The cmd, min and max
  222. fields are necessary only for unknown devices (currently <a
  223. href="#FHT8V">FHT8V</a> is "known"). cmd specifies the command name for the
  224. actor, min the minimum value and max the maximum value. The complete
  225. definition for an FHT8V device is:<code>fht8v_dev:valve:0:100</code>
  226. <br><br>
  227. p, i and d are the parameters use to controlling, see also the <a
  228. href="http://de.wikipedia.org/wiki/Regler">this</a> wikipedia entry.
  229. The default values are around 25.5, 3 and 5.88, you probably need to tune
  230. these values. They can be also changed later.
  231. <br><br>
  232. Examples:
  233. <ul>
  234. <code>define wz_pid PID wz_th wz_fht8v</code><br>
  235. </ul>
  236. </ul>
  237. <br>
  238. <a name="PIDset"></a>
  239. <b>Set </b>
  240. <ul>
  241. <li>set &lt;name&gt; factors p i d<br>
  242. Set the p, i and d factors, as described above.
  243. </li>
  244. <li>set &lt;name&gt; desired &lt;value&gt;<br>
  245. Set the desired value (e.g. temperature). Note: until this value is not
  246. set, no command is issued.
  247. </li>
  248. </ul>
  249. <br>
  250. <a name="PIDget"></a>
  251. <b>Get </b>
  252. <ul>
  253. N/A
  254. </ul>
  255. <br>
  256. <a name="PIDattr"></a>
  257. <b>Attributes</b>
  258. <ul>
  259. <li><a href="#disable">disable</a></li>
  260. <a name="roundValveValue"></a>
  261. <li>roundValveValue<br>
  262. round the valve value to an integer, of the attribute is set to 1.
  263. The valve value is automatically rounded, if the attribtue is not set,
  264. and the difference between min and max is greater than 100.
  265. </li><br>
  266. </ul>
  267. <br>
  268. </ul>
  269. =end html
  270. =cut