98_PID20.pm 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. # $Id: 98_PID20.pm 10722 2016-02-04 17:12:18Z john99sr $
  2. ####################################################################################################
  3. #
  4. # 98_PID20.pm
  5. # The PID device is a loop controller, used to set the value e.g of a heating
  6. # valve dependent of the current and desired temperature.
  7. #
  8. # This module is derived from the contrib/99_PID by Alexander Titzel.
  9. # The framework of the module is derived from proposals by betateilchen.
  10. #
  11. # This file is part of fhem.
  12. #
  13. # Fhem is free software: you can redistribute it and/or modify
  14. # it under the terms of the GNU General Public License as published by
  15. # the Free Software Foundation, either version 2 of the License, or
  16. # (at your option) any later version.
  17. #
  18. # Fhem is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. #
  23. # You should have received a copy of the GNU General Public License
  24. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  25. #
  26. #
  27. # V 1.00.c
  28. # 03.12.2013 - bugfix : pidActorLimitUpper wrong assignment
  29. # V 1.00.d
  30. # 09.12.2013 - verbose-level adjusted
  31. # 20.12.2013 - bugfix: actorErrorPos: wrong assignment by pidCalcInterval-attribute, if defined
  32. # V 1.00.e
  33. # 01.01.2014 - fix: {helper}{actorCommand} assigned to an emptry string if not defined
  34. # V 1.00.f
  35. # 22.01.2014 fix:pidDeltaTreshold only int was assignable, now even float
  36. # V 1.00.g
  37. # 29.01.2014 fix:calculation of i-portion is independent from pidCalcInterval
  38. # V 1.00.h
  39. # 26.02.2014 fix:new logging format; adjusting verbose-levels
  40. #
  41. # 26.03.2014 (betateilchen)
  42. # code review, pod added, removed old version info (will be provided via SVN)
  43. # V 1.01
  44. # 19.10.2014 fix in sub PID20_Set : wrong parameter $hash instead of $name, when calling PID20_Calc
  45. # V 1.0.0.2 30.10.14 - fix in PID20_Calc when setting $retStr (thanks to Thorsten Pferdekaemper)
  46. # V 1.0.0.3
  47. # 20.11.2014 Bug: processing of D-Portion and handling of disable
  48. # V 1.0.0.4
  49. # 27.11.2014 Change: all readings are updated cyclically
  50. # V 1.0.0.5
  51. # 06.11.2015 Fix: reg expression in sub PID20_Notify modifed. Thanks to user gero
  52. # V 1.0.0.6
  53. # 12.11.2015 Fix: wrong evaluation of Sensor String, when value was 0 than sensor timeout was detected
  54. # Feature: added <Internal> VERSION
  55. # V 1.0.0.7 Feature: new attribute pidActorCallBeforeSetting for user specific callback
  56. # before setting a new value to actor
  57. # V 1.0.0.8 Feature: attribute handling accelerates some actions
  58. # Fix: update of state triggers force a notify
  59. # Fix: adjust commandref
  60. # V 1.0.0.9 Feature: new attribute pidIPortionCallBeforeSetting for user specific callback
  61. # before setting a new value to p-portion
  62. ####################################################################################################
  63. package main;
  64. use strict;
  65. use warnings;
  66. use feature qw/say switch/;
  67. use vars qw(%defs);
  68. use vars qw($readingFnAttributes);
  69. use vars qw(%attr);
  70. use vars qw(%modules);
  71. my $PID20_Version = "1.0.0.9";
  72. sub PID20_Calc($);
  73. ########################################
  74. sub PID20_Log($$$)
  75. {
  76. my ( $hash, $loglevel, $text ) = @_;
  77. my $xline = ( caller(0) )[2];
  78. my $xsubroutine = ( caller(1) )[3];
  79. my $sub = ( split( ':', $xsubroutine ) )[2];
  80. $sub = substr( $sub, 6 ); # without PID20
  81. my $instName = ( ref($hash) eq "HASH" ) ? $hash->{NAME} : "PID20";
  82. Log3 $hash, $loglevel, "PID20 $instName: $sub.$xline " . $text;
  83. }
  84. ########################################
  85. sub PID20_Initialize($)
  86. {
  87. my ($hash) = @_;
  88. $hash->{DefFn} = 'PID20_Define';
  89. $hash->{UndefFn} = 'PID20_Undef';
  90. $hash->{SetFn} = 'PID20_Set';
  91. $hash->{GetFn} = 'PID20_Get';
  92. $hash->{NotifyFn} = 'PID20_Notify';
  93. $hash->{AttrFn} = 'PID20_Attr';
  94. $hash->{AttrList} =
  95. 'pidActorValueDecPlaces:0,1,2,3,4,5 '
  96. . 'pidActorInterval '
  97. . 'pidActorTreshold '
  98. . 'pidActorErrorAction:freeze,errorPos '
  99. . 'pidActorErrorPos '
  100. . 'pidActorKeepAlive '
  101. . 'pidActorLimitLower '
  102. . 'pidActorLimitUpper '
  103. . 'pidActorCallBeforeSetting '
  104. . 'pidIPortionCallBeforeSetting '
  105. . 'pidCalcInterval '
  106. . 'pidDeltaTreshold '
  107. . 'pidDesiredName '
  108. . 'pidFactor_P '
  109. . 'pidFactor_I '
  110. . 'pidFactor_D '
  111. . 'pidMeasuredName '
  112. . 'pidSensorTimeout '
  113. . 'pidReverseAction '
  114. . 'pidUpdateInterval '
  115. . 'pidDebugSensor:0,1 '
  116. . 'pidDebugActuation:0,1 '
  117. . 'pidDebugCalc:0,1 '
  118. . 'pidDebugDelta:0,1 '
  119. . 'pidDebugUpdate:0,1 '
  120. . 'pidDebugNotify:0,1 '
  121. . 'disable:0,1 '
  122. . $readingFnAttributes;
  123. }
  124. ##########################
  125. sub PID20_RestartTimer($$)
  126. {
  127. my ( $hash, $seconds ) = @_;
  128. my $name = $hash->{NAME};
  129. $seconds = 1 if ( $seconds <= 0 );
  130. RemoveInternalTimer($name);
  131. my $sdNextScan = gettimeofday() + $seconds;
  132. InternalTimer( $sdNextScan, 'PID20_Calc', $name, 0 );
  133. PID20_Log $hash, 5, 'name:'.$name.' seconds:'.$seconds;
  134. }
  135. ########################################
  136. sub PID20_TimeDiff($)
  137. {
  138. my ($strTS) = @_;
  139. #my ( $package, $filename, $line ) = caller(0);
  140. #print "PID $strTS line $line \n";
  141. my $serTS = ( defined($strTS) && $strTS ne "" ) ? time_str2num($strTS) : gettimeofday();
  142. my $timeDiff = gettimeofday() - $serTS;
  143. $timeDiff = 0 if ( $timeDiff < 0 );
  144. return $timeDiff;
  145. }
  146. ########################################
  147. sub PID20_Define($$$)
  148. {
  149. my ( $hash, $def ) = @_;
  150. my @a = split( "[ \t][ \t]*", $def );
  151. my $name = $a[0];
  152. my $reFloat = '^([\\+,\\-]?\\d+\\.?\d*$)'; # gleitpunkt
  153. if ( @a != 4 )
  154. {
  155. return "wrong syntax: define <name> PID20 " . "<sensor>:reading:[regexp] <actor>[:cmd] ";
  156. }
  157. ###################
  158. # Sensor
  159. my ( $sensor, $reading, $regexp ) = split( ":", $a[2], 3 );
  160. # if sensor unkonwn
  161. if ( !$defs{$sensor} )
  162. {
  163. my $msg = "$name: Unknown sensor device $sensor specified";
  164. PID20_Log $hash, 1, $msg;
  165. return $msg;
  166. }
  167. # if reading of sender is unkown
  168. if ( ReadingsVal( $sensor, $reading, 'unknown' ) eq 'unkown' )
  169. {
  170. my $msg = "$name: Unknown reading $reading for sensor device $sensor specified";
  171. PID20_Log $hash, 1, $msg;
  172. return $msg;
  173. }
  174. $hash->{helper}{sensor} = $sensor;
  175. $hash->{VERSION} = $PID20_Version;
  176. # defaults for regexp
  177. if ( !$regexp )
  178. {
  179. $regexp = $reFloat;
  180. }
  181. $hash->{helper}{reading} = $reading;
  182. $hash->{helper}{regexp} = $regexp;
  183. # Actor
  184. my ( $actor, $cmd ) = split( ":", $a[3], 2 );
  185. if ( !$defs{$actor} )
  186. {
  187. my $msg = "$name: Unknown actor device $actor specified";
  188. PID20_Log $hash, 1, $msg;
  189. return $msg;
  190. }
  191. $hash->{helper}{actor} = $actor;
  192. $hash->{helper}{actorCommand} = ( defined($cmd) ) ? $cmd : '';
  193. $hash->{helper}{stopped} = 0;
  194. $hash->{helper}{adjust} = '';
  195. $modules{PID20}{defptr}{$name} = $hash;
  196. readingsSingleUpdate( $hash, 'state', 'initializing', 1 );
  197. RemoveInternalTimer($name);
  198. InternalTimer( gettimeofday() + 10, 'PID20_Calc', $name, 0 );
  199. return undef;
  200. }
  201. ########################################
  202. sub PID20_Undef($$)
  203. {
  204. my ( $hash, $arg ) = @_;
  205. RemoveInternalTimer( $hash->{NAME} );
  206. return undef;
  207. }
  208. ########################################
  209. # we need a gradient for delta as base for d-portion calculation
  210. #
  211. sub PID20_Notify($$)
  212. {
  213. my ( $hash, $dev ) = @_;
  214. my $name = $hash->{NAME};
  215. my $sensorName = $hash->{helper}{sensor};
  216. my $DEBUG = AttrVal( $name, 'pidDebugNotify', '0' ) eq '1';
  217. my $disable = AttrVal( $name, 'disable', '0' );
  218. # no action if disabled
  219. if ( $disable eq '1' )
  220. {
  221. return '';
  222. }
  223. return if ( $dev->{NAME} ne $sensorName );
  224. my $sensorReadingName = $hash->{helper}{reading};
  225. my $regexp = $hash->{helper}{regexp};
  226. my $desiredName = AttrVal( $name, 'pidDesiredName', 'desired' );
  227. my $desired = ReadingsVal( $name, $desiredName, undef );
  228. my $max = int( @{ $dev->{CHANGED} } );
  229. PID20_Log $hash, 4, "check $max readings for " . $sensorReadingName;
  230. for ( my $i = 0 ; $i < $max ; $i++ )
  231. {
  232. my $s = $dev->{CHANGED}[$i];
  233. # continue, if no match with reading-name
  234. $s = '' if ( !defined($s) );
  235. PID20_Log $hash, 5, "check event:<$s>";
  236. next if ( $s !~ m/^$sensorReadingName:.*$/ );
  237. # ---- build difference current - old value
  238. # get sensor value
  239. my $sensorStr = ReadingsVal( $sensorName, $sensorReadingName, undef );
  240. $sensorStr =~ m/$regexp/;
  241. my $sensorValue = $1;
  242. # calc difference of delta/deltaOld
  243. my $delta = $desired - $sensorValue if ( defined($desired) );
  244. my $deltaOld = ( $hash->{helper}{deltaOld} + 0 ) if ( defined( $hash->{helper}{deltaOld} ) );
  245. my $deltaDiff = ( $delta - $deltaOld ) if ( defined($delta) && defined($deltaOld) );
  246. PID20_Log $hash, 5,
  247. "Diff: delta["
  248. . sprintf( "%.2f", $delta ) . "]"
  249. . " - deltaOld["
  250. . sprintf( "%.2f", $deltaOld ) . "]"
  251. . "= Diff["
  252. . sprintf( "%.2f", $deltaDiff ) . "]"
  253. if ($DEBUG);
  254. # ----- build difference of timestamps (ok)
  255. my $deltaOldTsStr = $hash->{helper}{deltaOldTS};
  256. my $deltaOldTsNum = time_str2num($deltaOldTsStr) if ( defined($deltaOldTsStr) );
  257. my $nowTsNum = gettimeofday();
  258. my $tsDiff = ( $nowTsNum - $deltaOldTsNum )
  259. if ( defined($deltaOldTsNum) && ( ( $nowTsNum - $deltaOldTsNum ) > 0 ) );
  260. PID20_Log $hash, 5, "tsDiff: tsDiff = $tsDiff " if ($DEBUG);
  261. # ----- calculate gradient of delta
  262. my $deltaGradient = $deltaDiff / $tsDiff
  263. if ( defined($deltaDiff) && defined($tsDiff) && ( $tsDiff > 0 ) );
  264. $deltaGradient = 0 if ( !defined($deltaGradient) );
  265. my $sdeltaDiff = ($deltaDiff) ? sprintf( "%.2f", $deltaDiff ) : '';
  266. my $sTSDiff = ($tsDiff) ? sprintf( "%.2f", $tsDiff ) : '';
  267. my $sDeltaGradient = ($deltaGradient) ? sprintf( "%.6f", $deltaGradient ) : '';
  268. PID20_Log $hash, 5,
  269. "deltaGradient: (Diff[$sdeltaDiff]" . "/tsDiff[$sTSDiff]" . "=deltaGradient per sec [$sDeltaGradient]"
  270. if ($DEBUG);
  271. # ----- store results
  272. $hash->{helper}{deltaGradient} = $deltaGradient;
  273. $hash->{helper}{deltaOld} = $delta;
  274. $hash->{helper}{deltaOldTS} = TimeNow();
  275. last;
  276. }
  277. return '';
  278. }
  279. ########################################
  280. sub PID20_Get($@)
  281. {
  282. my ( $hash, @a ) = @_;
  283. my $name = $hash->{NAME};
  284. my $usage = "Unknown argument $a[1], choose one of params:noArg";
  285. return $usage if ( @a < 2 );
  286. my $cmd = lc( $a[1] );
  287. given ($cmd)
  288. {
  289. when ('params')
  290. {
  291. my $ret = "Defined parameters for PID20 $name:\n\n";
  292. $ret .= 'Actor name : ' . $hash->{helper}{actor} . "\n";
  293. $ret .= 'Actor cmd : ' . $hash->{helper}{actorCommand} . "\n\n";
  294. $ret .= 'Sensor name : ' . $hash->{helper}{sensor} . "\n";
  295. $ret .= 'Sensor reading : ' . $hash->{helper}{reading} . "\n\n";
  296. $ret .= 'Sensor regexp : ' . $hash->{helper}{regexp} . "\n\n";
  297. $ret .= 'Factor P : ' . $hash->{helper}{factor_P} . "\n";
  298. $ret .= 'Factor I : ' . $hash->{helper}{factor_I} . "\n";
  299. $ret .= 'Factor D : ' . $hash->{helper}{factor_D} . "\n\n";
  300. $ret .= 'Actor lower limit: ' . $hash->{helper}{actorLimitLower} . "\n";
  301. $ret .= 'Actor upper limit: ' . $hash->{helper}{actorLimitUpper} . "\n";
  302. return $ret;
  303. }
  304. default { return $usage; }
  305. }
  306. }
  307. ########################################
  308. sub PID20_Set($@)
  309. {
  310. my ( $hash, @a ) = @_;
  311. my $name = $hash->{NAME};
  312. my $reFloat = '^([\\+,\\-]?\\d+\\.?\d*$)';
  313. my $usage =
  314. "Unknown argument $a[1], choose one of stop:noArg start:noArg restart "
  315. . AttrVal( $name, 'pidDesiredName', 'desired' );
  316. return $usage if ( @a < 2 );
  317. my $cmd = lc( $a[1] );
  318. my $desiredName = lc( AttrVal( $name, 'pidDesiredName', 'desired' ) );
  319. #PID20_Log $hash, 3, "name:$name cmd:$cmd $desired:$desired";
  320. given ($cmd)
  321. {
  322. when ('?')
  323. {
  324. return $usage;
  325. }
  326. when ($desiredName)
  327. {
  328. return "Set " . AttrVal( $name, 'pidDesiredName', 'desired' ) . " needs a <value> parameter"
  329. if ( @a != 3 );
  330. my $value = $a[2];
  331. $value = ( $value =~ m/$reFloat/ ) ? $1 : undef;
  332. return "value " . $a[2] . " is not a number"
  333. if ( !defined($value) );
  334. readingsSingleUpdate( $hash, $cmd, $value, 1 );
  335. PID20_Log $hash, 3, "set $name $cmd $a[2]";
  336. }
  337. when ('start')
  338. {
  339. return 'Set start needs a <value> parameter'
  340. if ( @a != 2 );
  341. $hash->{helper}{stopped} = 0;
  342. PID20_RestartTimer($hash,1);
  343. }
  344. when ('stop')
  345. {
  346. return 'Set stop needs a <value> parameter'
  347. if ( @a != 2 );
  348. $hash->{helper}{stopped} = 1;
  349. PID20_RestartTimer($hash,1);
  350. }
  351. when ('restart')
  352. {
  353. return 'Set restart needs a <value> parameter'
  354. if ( @a != 3 );
  355. my $value = $a[2];
  356. $value = ( $value =~ m/$reFloat/ ) ? $1 : undef;
  357. #PID20_Log $hash, 1, "value:$value";
  358. return "value " . $a[2] . " is not a number"
  359. if ( !defined($value) );
  360. $hash->{helper}{stopped} = 0;
  361. $hash->{helper}{adjust} = $value;
  362. PID20_RestartTimer($hash,1);
  363. PID20_Log $hash, 3, "set $name $cmd $value";
  364. }
  365. when ("calc") # inofficial function, only for debugging purposes
  366. {
  367. PID20_Calc($name);
  368. }
  369. default
  370. {
  371. return $usage;
  372. }
  373. }
  374. return;
  375. }
  376. ########################################
  377. # disabled = 0
  378. # idle = 1
  379. # processing = 2
  380. # stopped = 3
  381. # alarm = 4
  382. sub PID20_Calc($)
  383. {
  384. my $reUINT = '^([\\+]?\\d+)$'; # uint without whitespaces
  385. my $re01 = '^([0,1])$'; # only 0,1
  386. my $reINT = '^([\\+,\\-]?\\d+$)'; # int
  387. my $reFloatpos = '^([\\+]?\\d+\\.?\d*$)'; # gleitpunkt positiv float
  388. my $reFloat = '^([\\+,\\-]?\\d+\\.?\d*$)'; # float
  389. my ($name) = @_;
  390. my $hash = $defs{$name};
  391. my $sensor = $hash->{helper}{sensor};
  392. my $reading = $hash->{helper}{reading};
  393. my $regexp = $hash->{helper}{regexp};
  394. my $DEBUG_Sensor = AttrVal( $name, 'pidDebugSensor', '0' ) eq '1';
  395. my $DEBUG_Actuation = AttrVal( $name, 'pidDebugActuation', '0' ) eq '1';
  396. my $DEBUG_Delta = AttrVal( $name, 'pidDebugDelta', '0' ) eq '1';
  397. my $DEBUG_Calc = AttrVal( $name, 'pidDebugCalc', '0' ) eq '1';
  398. my $DEBUG_Update = AttrVal( $name, 'pidDebugUpdate', '0' ) eq '1';
  399. my $DEBUG = $DEBUG_Sensor || $DEBUG_Actuation || $DEBUG_Calc || $DEBUG_Delta || $DEBUG_Update;
  400. my $actuation = '';
  401. my $actuationDone = ReadingsVal( $name, 'actuation', '' );
  402. my $actuationCalc = ReadingsVal( $name, 'actuationCalc', '' );
  403. my $actuationCalcOld = $actuationCalc;
  404. my $actorTimestamp =
  405. ( $hash->{helper}{actorTimestamp} )
  406. ? $hash->{helper}{actorTimestamp}
  407. : FmtDateTime( gettimeofday() - 3600 * 24 );
  408. my $sensorStr = ReadingsVal( $sensor, $reading, '' );
  409. my $sensorValue = '';
  410. my $sensorTS = ReadingsTimestamp( $sensor, $reading, undef );
  411. my $sensorIsAlive = 0;
  412. my $iPortion = ReadingsVal( $name, 'p_i', 0 );
  413. my $pPortion = ReadingsVal( $name, 'p_p', '' );
  414. my $dPortion = ReadingsVal( $name, 'p_d', '' );
  415. my $stateStr = '';
  416. my $deltaOld = ReadingsVal( $name, 'delta', 0 );
  417. my $delta = '';
  418. my $deltaGradient = ( $hash->{helper}{deltaGradient} ) ? $hash->{helper}{deltaGradient} : 0;
  419. my $calcReq = 0;
  420. my $readingUpdateReq = '';
  421. # ---------------- check conditions
  422. while (1)
  423. {
  424. # --------------- retrive values from attributes
  425. $hash->{helper}{actorInterval} = ( AttrVal( $name, 'pidActorInterval', 180 ) =~ m/$reUINT/ ) ? $1 : 180;
  426. $hash->{helper}{actorThreshold} = ( AttrVal( $name, 'pidActorTreshold', 1 ) =~ m/$reUINT/ ) ? $1 : 1;
  427. $hash->{helper}{actorKeepAlive} = ( AttrVal( $name, 'pidActorKeepAlive', 1800 ) =~ m/$reUINT/ ) ? $1 : 1800;
  428. $hash->{helper}{actorValueDecPlaces} = ( AttrVal( $name, 'pidActorValueDecPlaces', 0 ) =~ m/$reUINT/ ) ? $1 : 0;
  429. $hash->{helper}{actorErrorAction} =
  430. ( AttrVal( $name, 'pidActorErrorAction', 'freeze' ) eq 'errorPos' ) ? 'errorPos' : 'freeze';
  431. $hash->{helper}{actorErrorPos} = ( AttrVal( $name, 'pidActorErrorPos', 0 ) =~ m/$reINT/ ) ? $1 : 0;
  432. $hash->{helper}{calcInterval} = ( AttrVal( $name, 'pidCalcInterval', 60 ) =~ m/$reUINT/ ) ? $1 : 60;
  433. $hash->{helper}{deltaTreshold} = ( AttrVal( $name, 'pidDeltaTreshold', 0 ) =~ m/$reFloatpos/ ) ? $1 : 0;
  434. $hash->{helper}{disable} = ( AttrVal( $name, 'disable', 0 ) =~ m/$re01/ ) ? $1 : '';
  435. $hash->{helper}{sensorTimeout} = ( AttrVal( $name, 'pidSensorTimeout', 3600 ) =~ m/$reUINT/ ) ? $1 : 3600;
  436. $hash->{helper}{reverseAction} = ( AttrVal( $name, 'pidReverseAction', 0 ) =~ m/$re01/ ) ? $1 : 0;
  437. $hash->{helper}{updateInterval} = ( AttrVal( $name, 'pidUpdateInterval', 600 ) =~ m/$reUINT/ ) ? $1 : 600;
  438. $hash->{helper}{measuredName} = AttrVal( $name, 'pidMeasuredName', 'measured' );
  439. $hash->{helper}{desiredName} = AttrVal( $name, 'pidDesiredName', 'desired' );
  440. $hash->{helper}{actorLimitLower} = ( AttrVal( $name, 'pidActorLimitLower', 0 ) =~ m/$reFloat/ ) ? $1 : 0;
  441. my $actorLimitLower = $hash->{helper}{actorLimitLower};
  442. $hash->{helper}{actorLimitUpper} = ( AttrVal( $name, 'pidActorLimitUpper', 100 ) =~ m/$reFloat/ ) ? $1 : 100;
  443. my $actorLimitUpper = $hash->{helper}{actorLimitUpper};
  444. $hash->{helper}{factor_P} = ( AttrVal( $name, 'pidFactor_P', 25 ) =~ m/$reFloatpos/ ) ? $1 : 25;
  445. $hash->{helper}{factor_I} = ( AttrVal( $name, 'pidFactor_I', 0.25 ) =~ m/$reFloatpos/ ) ? $1 : 0.25;
  446. $hash->{helper}{factor_D} = ( AttrVal( $name, 'pidFactor_D', 0 ) =~ m/$reFloatpos/ ) ? $1 : 0;
  447. if ( $hash->{helper}{disable} )
  448. {
  449. $stateStr = 'disabled';
  450. last;
  451. }
  452. if ( $hash->{helper}{stopped} )
  453. {
  454. $stateStr = 'stopped';
  455. last;
  456. }
  457. my $desired = ReadingsVal( $name, $hash->{helper}{desiredName}, '' );
  458. # sensor found
  459. PID20_Log $hash, 2, '--------------------------' if ($DEBUG);
  460. PID20_Log $hash, 2, "S1 sensorStr:$sensorStr sensorTS:$sensorTS" if ($DEBUG_Sensor);
  461. $stateStr = "alarm - no $reading yet for $sensor" if ( $sensorStr eq '' && $stateStr eq '' );
  462. # sensor alive
  463. if ( $sensorStr ne '' && $sensorTS )
  464. {
  465. my $timeDiff = PID20_TimeDiff($sensorTS);
  466. $sensorIsAlive = 1 if ( $timeDiff <= $hash->{helper}{sensorTimeout} );
  467. $sensorStr =~ m/$regexp/;
  468. $sensorValue = $1;
  469. $sensorValue = '' if ( !defined($sensorValue) );
  470. PID20_Log $hash, 2,
  471. 'S2 timeOfDay:'
  472. . gettimeofday()
  473. . " timeDiff:$timeDiff sensorTimeout:"
  474. . $hash->{helper}{sensorTimeout}
  475. . " --> sensorIsAlive:$sensorIsAlive"
  476. if ($DEBUG_Sensor);
  477. }
  478. # sensor dead
  479. $stateStr = 'alarm - dead sensor' if ( !$sensorIsAlive && $stateStr eq '' );
  480. # missing desired
  481. $stateStr = 'alarm - missing desired' if ( $desired eq '' && $stateStr eq '' );
  482. # check delta threshold
  483. $delta = ( $desired ne '' && $sensorValue ne '' ) ? $desired - $sensorValue : '';
  484. $calcReq = 1 if ( $stateStr eq '' && $delta ne '' && ( abs($delta) >= abs( $hash->{helper}{deltaTreshold} ) ) );
  485. PID20_Log $hash, 2,
  486. "D1 desired[" . ( $desired ne '' ) ? sprintf( "%.1f", $desired )
  487. : "" . "] - sensorValue: [" . ( $sensorValue ne '' ) ? sprintf( "%.1f", $sensorValue )
  488. : "" . "] = delta[" . ( $delta ne "" ) ? sprintf( "%.2f", $delta )
  489. : "" . "] calcReq:$calcReq"
  490. if ($DEBUG_Delta);
  491. #request for calculation
  492. # ---------------- calculation request
  493. if ($calcReq)
  494. {
  495. # reverse action requested
  496. my $workDelta = ( $hash->{helper}{reverseAction} == 1 ) ? -$delta : $delta;
  497. my $deltaOld = -$deltaOld if ( $hash->{helper}{reverseAction} == 1 );
  498. # calc p-portion
  499. $pPortion = $workDelta * $hash->{helper}{factor_P};
  500. # calc d-Portion
  501. $dPortion = ($deltaGradient) * $hash->{helper}{calcInterval} * $hash->{helper}{factor_D};
  502. # calc i-portion respecting windUp
  503. # freeze i-portion if windUp is active
  504. my $isWindup = $actuationCalcOld
  505. && ( ( $workDelta > 0 && $actuationCalcOld > $actorLimitUpper )
  506. || ( $workDelta < 0 && $actuationCalcOld < $actorLimitLower ) );
  507. if ( $hash->{helper}{adjust} ne '' )
  508. {
  509. $iPortion = $hash->{helper}{adjust} - ( $pPortion + $dPortion );
  510. $iPortion = $actorLimitUpper if ( $iPortion > $actorLimitUpper );
  511. $iPortion = $actorLimitLower if ( $iPortion < $actorLimitLower );
  512. PID20_Log $hash, 5, "adjust request with:" . $hash->{helper}{adjust} . " ==> p_i:$iPortion";
  513. $hash->{helper}{adjust} = '';
  514. } elsif ( !$isWindup ) # integrate only if no windUp
  515. {
  516. # normalize the intervall to minute=60 seconds
  517. $iPortion = $iPortion + $workDelta * $hash->{helper}{factor_I} * $hash->{helper}{calcInterval} / 60;
  518. $hash->{helper}{isWindUP} = 0;
  519. }
  520. $hash->{helper}{isWindUP} = $isWindup;
  521. # check callback for iPortion
  522. my $iportionCallBeforeSetting = AttrVal( $name, 'pidIPortionCallBeforeSetting', undef );
  523. if ( defined($iportionCallBeforeSetting) && exists &$iportionCallBeforeSetting )
  524. {
  525. PID20_Log $hash, 5, 'start callback ' . $iportionCallBeforeSetting . ' with iPortion:' . $iPortion;
  526. no strict "refs";
  527. $iPortion = &$iportionCallBeforeSetting( $name, $iPortion );
  528. use strict "refs";
  529. PID20_Log $hash, 5, 'return value of ' . $iportionCallBeforeSetting . ':' . $iPortion;
  530. }
  531. # calc actuation
  532. $actuationCalc = $pPortion + $iPortion + $dPortion;
  533. PID20_Log $hash, 2, 'P1 delta:' . sprintf( "%.2f", $delta ) . " isWindup:$isWindup" if ($DEBUG_Calc);
  534. PID20_Log $hash, 2,
  535. 'P2 pPortion:'
  536. . sprintf( "%.2f", $pPortion )
  537. . " iPortion:"
  538. . sprintf( "%.2f", $iPortion )
  539. . " dPortion:"
  540. . sprintf( "%.2f", $dPortion )
  541. . " actuationCalc:"
  542. . sprintf( "%.2f", $actuationCalc )
  543. if ($DEBUG_Calc);
  544. }
  545. $readingUpdateReq = 1; # in each case update readings
  546. # ---------------- acutation request
  547. my $noTrouble = ( $desired ne "" && $sensorIsAlive );
  548. # check actor fallback in case of sensor fault
  549. if ( !$sensorIsAlive && ( $hash->{helper}{actorErrorAction} eq "errorPos" ) )
  550. {
  551. $stateStr .= '- force pid-output to errorPos';
  552. $actuationCalc = $hash->{helper}{actorErrorPos};
  553. $actuationCalc = '' if ( !defined($actuationCalc) );
  554. }
  555. # check acutation diff
  556. $actuation = $actuationCalc;
  557. # limit $actuation
  558. $actuation = $actorLimitUpper if ( $actuation ne '' && ( $actuation > $actorLimitUpper ) );
  559. $actuation = $actorLimitLower if ( $actuation ne '' && ( $actuation < $actorLimitLower ) );
  560. # check if round request
  561. my $fmt = "%." . $hash->{helper}{actorValueDecPlaces} . "f";
  562. $actuation = sprintf( $fmt, $actuation ) if ( $actuation ne '' );
  563. my $actuationDiff = abs( $actuation - $actuationDone )
  564. if ( $actuation ne '' && $actuationDone ne '' );
  565. PID20_Log $hash, 2,
  566. "A1 act:$actuation actDone:$actuationDone "
  567. . " actThreshold:"
  568. . $hash->{helper}{actorThreshold}
  569. . " actDiff:$actuationDiff"
  570. if ($DEBUG_Actuation);
  571. # check threshold-condition for actuation
  572. my $rsTS = $actuationDone ne '' && $actuationDiff >= $hash->{helper}{actorThreshold};
  573. # ...... special handling if acutation is in the black zone between actorLimit and (actorLimit - actorThreshold)
  574. # upper range
  575. my $rsUp =
  576. $actuationDone ne ''
  577. && $actuation > $actorLimitUpper - $hash->{helper}{actorThreshold}
  578. && $actuationDiff != 0
  579. && $actuation >= $actorLimitUpper;
  580. # low range
  581. my $rsDown =
  582. $actuationDone ne ''
  583. && $actuation < $actorLimitLower + $hash->{helper}{actorThreshold}
  584. && $actuationDiff != 0
  585. && $actuation <= $actorLimitLower;
  586. # upper or lower limit are exceeded
  587. my $rsLimit = $actuationDone ne '' && ( $actuationDone < $actorLimitLower || $actuationDone > $actorLimitUpper );
  588. my $actuationByThreshold = ( ( $rsTS || $rsUp || $rsDown ) && $noTrouble );
  589. PID20_Log $hash, 2, "A2 rsTS:$rsTS rsUp:$rsUp rsDown:$rsDown noTrouble:$noTrouble"
  590. if ($DEBUG_Actuation);
  591. # check time condition for actuation
  592. my $actTimeDiff = PID20_TimeDiff($actorTimestamp); # $actorTimestamp is valid in each case
  593. my $actuationByTime = ($noTrouble) && ( $actTimeDiff > $hash->{helper}{actorInterval} );
  594. PID20_Log $hash, 2,
  595. "A3 actTS:$actorTimestamp"
  596. . " actTimeDiff:"
  597. . sprintf( "%.2f", $actTimeDiff )
  598. . " actInterval:"
  599. . $hash->{helper}{actorInterval}
  600. . "-->actByTime:$actuationByTime "
  601. if ($DEBUG_Actuation);
  602. # check keep alive condition for actuation
  603. my $actuationKeepAliveReq = ( $actTimeDiff >= $hash->{helper}{actorKeepAlive} )
  604. if ( defined($actTimeDiff) && $actuation ne "" );
  605. # build total actuation request
  606. my $actuationReq = (
  607. ( $actuationByThreshold && $actuationByTime )
  608. || $actuationKeepAliveReq # request by keep alive
  609. || $rsLimit # upper or lower limit are exceeded
  610. || $actuationDone eq '' # startup condition
  611. ) && $actuation ne ''; # acutation is initialized
  612. PID20_Log $hash, 2,
  613. "A4 (actByTh:$actuationByThreshold && actByTime:$actuationByTime)"
  614. . "||actKeepAlive:$actuationKeepAliveReq"
  615. . "||rsLimit:$rsLimit=actnReq:$actuationReq"
  616. if ($DEBUG_Actuation);
  617. # ................ perform output to actor
  618. if ($actuationReq)
  619. {
  620. $readingUpdateReq = 1; # update the readings
  621. # check calback for actuation
  622. my $actorCallBeforeSetting = AttrVal( $name, 'pidActorCallBeforeSetting', undef );
  623. if ( defined($actorCallBeforeSetting) && exists &$actorCallBeforeSetting )
  624. {
  625. PID20_Log $hash, 5, 'start callback ' . $actorCallBeforeSetting . ' with actuation:' . $actuation;
  626. no strict "refs";
  627. $actuation = &$actorCallBeforeSetting( $name, $actuation );
  628. use strict "refs";
  629. PID20_Log $hash, 5, 'return value of ' . $actorCallBeforeSetting . ':' . $actuation;
  630. }
  631. #build command for fhem
  632. PID20_Log $hash, 5, 'actor:' . $hash->{helper}{actor} . ' actorCommand:' . $hash->{helper}{actorCommand}
  633. . ' actuation:' . $actuation;
  634. my $cmd = sprintf( "set %s %s %g", $hash->{helper}{actor}, $hash->{helper}{actorCommand}, $actuation );
  635. # execute command
  636. my $ret;
  637. $ret = fhem $cmd;
  638. # note timestamp
  639. $hash->{helper}{actorTimestamp} = TimeNow();
  640. $actuationDone = $actuation;
  641. my $retStr = '';
  642. $retStr = ' with return-value:' . $ret if ( defined($ret) && ( $ret ne '' ) );
  643. PID20_Log $hash, 3, "<$cmd> " . $retStr;
  644. }
  645. my $updateAlive = ( $actuation ne '' )
  646. && PID20_TimeDiff( ReadingsTimestamp( $name, 'actuation', gettimeofday() ) ) >= $hash->{helper}{updateInterval};
  647. # my $updateReq = ( ( $actuationReq || $updateAlive ) && $actuation ne "" );
  648. # PID20_Log $hash, 2, "U1 actReq:$actuationReq updateAlive:$updateAlive --> updateReq:$updateReq" if ($DEBUG_Update);
  649. # ---------------- update request
  650. if ($readingUpdateReq)
  651. {
  652. readingsBeginUpdate($hash);
  653. readingsBulkUpdate( $hash, $hash->{helper}{desiredName}, $desired ) if ( $desired ne '' );
  654. readingsBulkUpdate( $hash, $hash->{helper}{measuredName}, $sensorValue ) if ( $sensorValue ne '' );
  655. readingsBulkUpdate( $hash, 'p_p', $pPortion ) if ( $pPortion ne '' );
  656. readingsBulkUpdate( $hash, 'p_d', $dPortion ) if ( $dPortion ne '' );
  657. readingsBulkUpdate( $hash, 'p_i', $iPortion ) if ( $iPortion ne '' );
  658. readingsBulkUpdate( $hash, 'actuation', $actuationDone ) if ( $actuationDone ne '' );
  659. readingsBulkUpdate( $hash, 'actuationCalc', $actuationCalc ) if ( $actuationCalc ne '' );
  660. readingsBulkUpdate( $hash, 'delta', $delta ) if ( $delta ne '' );
  661. readingsEndUpdate( $hash, 1 );
  662. PID20_Log $hash, 5, "readings updated";
  663. }
  664. last;
  665. } # end while
  666. # ........ update statePID.
  667. $stateStr = 'idle' if ( $stateStr eq '' && !$calcReq );
  668. $stateStr = 'processing' if ( $stateStr eq '' && $calcReq );
  669. readingsSingleUpdate( $hash, 'state', $stateStr, 1 );
  670. PID20_Log $hash, 2, "C1 stateStr:$stateStr calcReq:$calcReq" if ($DEBUG_Calc);
  671. #......... timer setup
  672. my $next = gettimeofday() + $hash->{helper}{calcInterval};
  673. RemoveInternalTimer($name); # prevent multiple timers for same hash
  674. InternalTimer( $next, 'PID20_Calc', $name, 1 );
  675. #PID20_Log $hash, 2, "InternalTimer next:".FmtDateTime($next)." PID20_Calc name:$name DEBUG_Calc:$DEBUG_Calc";
  676. return;
  677. }
  678. ########################################
  679. # attribute handling
  680. sub PID20_Attr($$$$)
  681. {
  682. my ( $command, $name, $attribute, $value ) = @_;
  683. my $msg = undef;
  684. my $hash = $defs{$name};
  685. my $reUINT = '^([\\+]?\\d+)$';
  686. PID20_Log $hash, 5, 'name:' . $name . ' attribute:' . $attribute . ' value:' . $value . ' command:' . $command;
  687. if ( $attribute eq 'disable' )
  688. {
  689. # PID20_Log $hash, 5, 'disable';
  690. PID20_RestartTimer($hash,2);
  691. }
  692. return $msg;
  693. }
  694. 1;
  695. =pod
  696. =begin html
  697. <a name="PID20"></a>
  698. <h3>PID20</h3>
  699. <ul>
  700. <a name="PID20define"></a>
  701. <b>Define</b>
  702. <ul>
  703. <br/>
  704. <code>define &lt;name&gt; PID20 &lt;sensor[:reading[:regexp]]&gt; &lt;actor:cmd &gt;</code>
  705. <br/><br/>
  706. This module provides a PID device, using &lt;sensor&gt; and &lt;actor&gt;<br/>
  707. </ul>
  708. <br/><br/>
  709. <a name="PID20set"></a>
  710. <b>Set-Commands</b><br/>
  711. <ul>
  712. <br/>
  713. <code>set &lt;name&gt; desired &lt;value&gt;</code>
  714. <br/><br/>
  715. <ul>Set desired value for PID</ul>
  716. <br/>
  717. <br/>
  718. <code>set &lt;name&gt; start</code>
  719. <br/><br/>
  720. <ul>Start PID processing again, using frozen values from former stop.</ul>
  721. <br/>
  722. <br/>
  723. <code>set &lt;name&gt; stop</code>
  724. <br/><br/>
  725. <ul>PID stops processing, freezing all values.</ul>
  726. <br/>
  727. <br/>
  728. <code>set &lt;name&gt; restart &lt;value&gt;</code>
  729. <br/><br/>
  730. <ul>Same as start, but uses value as start value for actor</ul>
  731. <br/>
  732. </ul>
  733. <br/><br/>
  734. <a name="PID20get"></a>
  735. <b>Get-Commands</b><br/>
  736. <ul>
  737. <br/>
  738. <code>get &lt;name&gt; params</code>
  739. <br/><br/>
  740. <ul>Get list containing current parameters.</ul>
  741. <br/>
  742. </ul>
  743. <br/><br/>
  744. <a name="PID20attr"></a>
  745. <b>Attributes</b><br/><br/>
  746. <ul>
  747. <li><a href="#readingFnAttributes">readingFnAttributes</a></li>
  748. <br/>
  749. <li><b>disable</b> - disable the PID device, possible values: 0,1; default: 0</li>
  750. <li><b>pidActorValueDecPlaces</b> - number of demicals, possible values: 0..5; default: 0</li>
  751. <li><b>pidActorInterval</b> - number of seconds to wait between to commands sent to actor; default: 180</li>
  752. <li><b>pidActorTreshold</b> - threshold to be reached before command will be sent to actor; default: 1</li>
  753. <li><b>pidActorErrorAction</b> - required action on error, possible values: freeze,errorPos; default: freeze</li>
  754. <li><b>pidActorErrorPos</b> - actor's position to be used in case of error; default: 0</li>
  755. <li><b>pidActorKeepAlive</b> - number of seconds to force command to be sent to actor; default: 1800</li>
  756. <li><b>pidActorLimitLower</b> - lower limit for actor; default: 0</li>
  757. <li><b>pidActorLimitUpper</b> - upper limit for actor; default: 100</li>
  758. <li><b>pidCalcInterval</b> - interval (seconds) to calculate new pid values; default: 60</li>
  759. <li><b>pidDeltaTreshold</b> - if delta < delta-threshold the pid will enter idle state; default: 0</li>
  760. <li><b>pidDesiredName</b> - reading's name for desired value; default: desired</li>
  761. <li><b>pidFactor_P</b> - P value for PID; default: 25</li>
  762. <li><b>pidFactor_I</b> - I value for PID; default: 0.25</li>
  763. <li><b>pidFactor_D</b> - D value for PID; default: 0</li>
  764. <li><b>pidMeasuredName</b> - reading's name for measured value; default: measured</li>
  765. <li><b>pidSensorTimeout</b> - number of seconds to wait before sensor will be recognized n/a; default: 3600</li>
  766. <li><b>pidReverseAction</b> - reverse PID operation mode, possible values: 0,1; default: 0</li>
  767. <li><b>pidUpdateInterval</b> - number of seconds to wait before an update will be forced for plotting; default: 300</li>
  768. <li><b>pidActorCallBeforeSetting</b> - an optional callback-function,which can manipulate the actorValue; default: not defined
  769. <pre>
  770. # Exampe for callback-function
  771. # 1. argument = name of PID20
  772. # 2. argument = current actor value
  773. sub PIDActorSet($$)
  774. {
  775. my ( $name, $actValue ) = @_;
  776. if ($actValue>70)
  777. {
  778. $actValue=100;
  779. }
  780. return $actValue;
  781. }</pre>
  782. </li>
  783. <li><b>pidIPortionCallBeforeSetting</b> - an optional callback-function, which can manipulate the value of I-Portion; default: not defined
  784. <pre>
  785. # Exampe for callback-function
  786. # 1. argument = name of PID20
  787. # 2. argument = current i-portion value
  788. sub PIDIPortionSet($$)
  789. {
  790. my ( $name, $actValue ) = @_;
  791. if ($actValue>70)
  792. {
  793. $actValue=70;
  794. }
  795. return $actValue;
  796. }</pre>
  797. </li>
  798. </ul>
  799. <br/><br/>
  800. <b>Generated Readings/Events:</b>
  801. <br/><br/>
  802. <ul>
  803. <li><b>actuation</b> - real actuation set to actor</li>
  804. <li><b>actuationCalc</b> - internal actuation calculated without limits</li>
  805. <li><b>delta</b> - current difference desired - measured</li>
  806. <li><b>desired</b> - desired value</li>
  807. <li><b>measured</b> - measured value</li>
  808. <li><b>p_p</b> - p value of pid calculation</li>
  809. <li><b>p_i</b> - i value of pid calculation</li>
  810. <li><b>p_d</b> - d value of pid calculation</li>
  811. <li><b>state</b> - current device state</li>
  812. <br/>
  813. Names for desired and measured readings can be changed by corresponding attributes (see above).<br/>
  814. </ul>
  815. <br/><br/>
  816. <b>Additional information</b><br/><br/>
  817. <ul>
  818. <li><a href="http://forum.fhem.de/index.php/topic,17067.0.html">Discussion in FHEM forum</a></li><br/>
  819. <li><a href="http://www.fhemwiki.de/wiki/PID20_-_Der_PID-Regler">Information in FHEM wiki</a></li><br/>
  820. </ul>
  821. </ul>
  822. =end html