93_PWMR.pm 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903
  1. #
  2. #
  3. # 93_PWMR.pm
  4. # written by Andreas Goebel 2012-07-25
  5. # e-mail: ag at goebel-it dot de
  6. #
  7. ##############################################
  8. # $Id: 93_PWMR.pm 16121 2018-02-08 14:55:52Z jamesgo $
  9. # 29.07.15 GA change set <name> manualTempDuration <minutes>
  10. # 21.09.15 GA update, use Log3 and readingsSingleUpdate
  11. # 07.10.15 GA initial version published
  12. # 07.10.15 GA fix calculation of PWMPulse, default for c_autoCalcTemp
  13. # 13.10.15 GA add event-on-change-reading
  14. # 14.10.15 GA fix round energyusedp
  15. # 15.10.15 GA add a_regexp_on, a regular expression for the on state of the actor
  16. # 05.11.15 GA fix new reading desired-temp-until which substitutes modification date of desired-temp in the future
  17. # events for desired-temp adjusted (no update of timestamp if temperature stays the same)
  18. # 10.11.15 GA fix event for actor change added again, desired-temp notifications adjusted for midnight change
  19. # 17.11.15 GA add ReadRoom will now set a reading named temperature containing the last temperature used for calculation
  20. # 18.11.15 GA add adjusted energyusedp to be in percent. Now it can be used in Tablet-UI as valve-position
  21. # 19.11.15 GA fix move actorState to readings
  22. # 22.11.15 GA fix rules on wednesday are now possible (thanks to Skusi)
  23. # 22.11.15 GA fix error handling in SetRoom (thanks to cobra112)
  24. # 30.11.15 GA fix set reading of desired-temp-used to frost_protect if window is opened
  25. # 30.11.15 GA add call PWMR_Attr in PWMR_Define if already some attributes are defined
  26. # 26.01.16 GA fix don't call AssignIoPort
  27. # 26.01.16 GA fix assign IODev as reference to that hash (otherwise xmllist will crash fhem)
  28. # 26.01.16 GA add implementation of PID regulation
  29. # 27.01.16 GA add attribute desiredTempFrom to take desiredTemp from another object
  30. # 04.02.16 GA add DLookBackCnt, buffer holding previouse temperatures used for PID D-Part calculation
  31. # 08.02.16 GA add ILookBackCnt, buffer holding previouse temperatures used for PID I-Part calculation
  32. # 08.02.16 GA add valueFormat attribute
  33. # 29.06.16 GA add "set frostProtect on|off"
  34. # 16.08.16 GA add event-min-interval
  35. # 23.09.16 GA fix changes on commandref based on suggestions from user "sledge"
  36. # 28.09.16 GA add readings for tempRules (single reading for Mo to So)
  37. # 04.10.16 GA fix adjust readings for tempRules if temperature changes
  38. # 11.10.16 GA fix delete log entries for PWMR_NormalizeRules
  39. # 17.10.16 GA fix attribute tempFrostProtect is now evaluated
  40. # 16.11.16 GA add display time until in state if "ManualSetUntil"
  41. # 16.11.16 GA fix format desired-temp with one digit after the decimal point
  42. # 17.11.16 GA add internals for configuration parameters: p_factor, p_tsensor, p_actor, p_window, p_pid
  43. # 11.12.16 GA add alternative PID calculation, selectable by usePID=2, implementation from user Albatros_
  44. # 14.12.16 GA fix adjust rounding of PVal and newpulsePID
  45. # 14.12.16 GA fix supply DBuffer with delta temps for usePID=2 calculation
  46. # 14.12.16 GA add implement get previousTemps
  47. # 01.08.17 GA add documentation for attribute disable
  48. # 27.12.17 GA add handle "off" as c_tempFrostProtect and "on" as c_tempC in getDesiredTempFrom (valid form Homematic)
  49. # 31.01.18 GA add support for stateFormat
  50. # 08.02.18 GA fix PID_I_previousTemps was shortened to c_PID_DLookBackCnt instead of c_PID_ILookBackCnt in define
  51. # module for PWM (Pulse Width Modulation) calculation
  52. # this module defines a room for calculation
  53. # it is used by a PWM object
  54. # reference to the PWM object is via IODev
  55. # PWMR object defines:
  56. # IODev: reference to PWM
  57. # factor (also used in Pulse calculation):
  58. # temperatur difference * factor * cycletime (from PWM) defines on/off periods (pulse)
  59. # sensor delivering the temperature (temperature is read from reading using a regexp)
  60. # actor to switch on/off the heating devices (may be a structure if more than on actor..)
  61. # comma separated list of window contacts followd by ":" and a regular expression
  62. # default for the regular expression is "Open"
  63. # if the regular expression matches on of the contacts
  64. # then readRoom will return c_tempFrostProtect as desired-temp
  65. # instead of the current calculated desired-temp
  66. # this should cause the calculation routine for the room to switch off heating
  67. #
  68. # calculation of "desired-temp" is done in a loop (5-minutes default)
  69. # - if c_frostProtect is "1" -> set to c_tempFrostProtect
  70. # - if c_autoCalcTemp is "1" -> use c_tempN, c_tempD, c_tempC, c_tempE and c_tempRule[1-5]
  71. # - c_* variables are syntax checked and derived from attr which have a readable syntax
  72. #
  73. # - c_tempRule[1-5] are processed in order 5..1 (5 is highes priority)
  74. # rules define:
  75. # <interval of valid days (0..6 = So..Sa)
  76. # <time>,[N|D|C] [<time>,[N|D|C|E]]
  77. # ... time is interpreted as Hour[:Min]
  78. # ... N,D,C,E reference timeN (Night), timeD (Day), timeC (Cosy), timeE(Energysave)
  79. #
  80. # attr names are: tempDay, tempCosy, tempNight, tempEnergy ...
  81. #
  82. # subroutines PWMR_ReedRoom and PWMR_SetRoom are called from PWM object
  83. package main;
  84. use strict;
  85. use warnings;
  86. my %dayno = (
  87. "mo" => 1,
  88. "di" => 2,
  89. "mi" => 3,
  90. "do" => 4,
  91. "fr" => 5,
  92. "sa" => 6,
  93. "so" => 0
  94. );
  95. sub PWMR_Get($@);
  96. sub PWMR_Set($@);
  97. sub PWMR_Define($$);
  98. sub PWMR_CalcDesiredTemp($);
  99. sub PWMR_SetRoom(@);
  100. sub PWMR_ReadRoom(@);
  101. sub PWMR_Attr(@);
  102. sub PWMR_Boost(@);
  103. sub PWMR_valueFormat(@);
  104. ###################################
  105. sub
  106. PWMR_Initialize($)
  107. {
  108. my ($hash) = @_;
  109. $hash->{GetFn} = "PWMR_Get";
  110. $hash->{SetFn} = "PWMR_Set";
  111. $hash->{DefFn} = "PWMR_Define";
  112. $hash->{UndefFn} = "PWMR_Undef";
  113. $hash->{AttrFn} = "PWMR_Attr";
  114. $hash->{AttrList} = "disable:1,0 loglevel:0,1,2,3,4,5 ".
  115. "frostProtect:0,1 ".
  116. "autoCalcTemp:0,1 ".
  117. "desiredTempFrom ".
  118. "tempFrostProtect ".
  119. "tempDay ".
  120. "tempNight ".
  121. "tempCosy ".
  122. "tempEnergy ".
  123. "tempRule1 ".
  124. "tempRule2 ".
  125. "tempRule3 ".
  126. "tempRule4 ".
  127. "tempRule5 ".
  128. "valueFormat:textField-long ".
  129. " ".$readingFnAttributes;
  130. }
  131. sub
  132. PWMR_getDesiredTempFrom(@)
  133. {
  134. my ($hash, $dt, $d_reading, $d_regexpTemp) = @_;
  135. my $newTemp;
  136. my $d_readingVal = defined($dt->{READINGS}{$d_reading}{VAL}) ? $dt->{READINGS}{$d_reading}{VAL} : "undef";
  137. my $val = $d_readingVal;
  138. $val =~ /$d_regexpTemp/;
  139. if (defined($1)) {
  140. $newTemp = $1;
  141. Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val)");
  142. } else { # regexp does not match
  143. if ($val =~ /^on$/) {
  144. $newTemp = $hash->{c_tempC};
  145. Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val) set to 30");
  146. } elsif ( $val =~ /^off$/ ) {
  147. $newTemp = $hash->{c_tempFrostProtect};
  148. Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val) set to frostProtect");
  149. } else {
  150. $newTemp = $hash->{c_tempFrostProtect};
  151. Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val) set to frostProtect");
  152. }
  153. }
  154. Log3 ($hash, 4, "PWMR_getDesiredTempFrom $hash->{NAME}: from $dt->{NAME} reading($d_reading) VAL($d_readingVal) regexp($d_regexpTemp) regexpVal($val)");
  155. return ($newTemp);
  156. }
  157. ###################################
  158. sub
  159. PWMR_CalcDesiredTemp($)
  160. {
  161. my ($hash) = @_;
  162. if($hash->{INTERVAL} > 0) {
  163. if ($hash->{INTERVAL} == 300) {
  164. # align interval to hh:00:ss, hh:05:ss, ... hh:55:ss
  165. my $n = gettimeofday();
  166. my ($hour, $min, $sec) = split (":", FmtTime($n));
  167. # 15:12:05 -> 15:16:05
  168. my $offset = ((((int($min/ 5)) +1 ) * 5 ) - $min) * 60;
  169. #Log3 ($hash, 4, "offset $min -> ".int($min / 5)." $offset ".($offset / 60));
  170. InternalTimer($n + $offset, "PWMR_CalcDesiredTemp", $hash, 0);
  171. } else {
  172. InternalTimer(gettimeofday()+$hash->{INTERVAL}, "PWMR_CalcDesiredTemp", $hash, 0);
  173. #Log3 ($hash, 4, "interval not 300");
  174. }
  175. }
  176. my $name = $hash->{NAME};
  177. if (defined($hash->{READINGS}{"desired-temp-until"})) {
  178. if ($hash->{READINGS}{"desired-temp-until"}{VAL} ne "no" ) {
  179. if ($hash->{READINGS}{"desired-temp-until"}{VAL} gt TimeNow()) {
  180. Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: desired-temp was manualy set until ".
  181. $hash->{READINGS}{"desired-temp"}{TIME});
  182. return undef;
  183. }
  184. else
  185. {
  186. readingsSingleUpdate ($hash, "desired-temp-until", "no", 1);
  187. Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: calc desired-temp");
  188. }
  189. }
  190. }
  191. ####################
  192. # frost protection
  193. if ($hash->{c_frostProtect} > 0) {
  194. if ($hash->{READINGS}{"desired-temp"}{VAL} != $hash->{c_tempFrostProtect}
  195. or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) {
  196. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $hash->{c_tempFrostProtect}), 1);
  197. } else {
  198. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $hash->{c_tempFrostProtect}), 0);
  199. }
  200. #$hash->{READINGS}{"desired-tem"}{TIME} = TimeNow();
  201. #$hash->{READINGS}{"desired-temp"}{VAL} = $hash->{c_tempFrostProtect};
  202. #push @{$hash->{CHANGED}}, "desired-temp $hash->{c_tempFrostProtect}";
  203. #DoTrigger($name, undef);
  204. #$hash->{STATE} = "FrostProtect";
  205. readingsSingleUpdate ($hash, "state", "FrostProtect", 1);
  206. return undef;
  207. }
  208. ####################
  209. # rule based calculation
  210. if ($hash->{c_autoCalcTemp} > 0 ) {
  211. if ($hash->{c_desiredTempFrom} eq "") {
  212. #$hash->{STATE} = "Calculating";
  213. readingsSingleUpdate ($hash, "state", "Calculating", 1);
  214. my @time = localtime();
  215. my $wday = $time[6];
  216. my $cmptime = sprintf ("%02d%02d", $time[2], $time[1]);
  217. Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: wday $wday cmptime $cmptime");
  218. foreach my $rule ($hash->{c_tempRule5},
  219. $hash->{c_tempRule4},
  220. $hash->{c_tempRule3},
  221. $hash->{c_tempRule2},
  222. $hash->{c_tempRule1} ) {
  223. if ($rule ne "") { # valid rule is 1-5 0600,D 1800,C 2200,N
  224. Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: $rule");
  225. my @points = split (" ", $rule);
  226. my ($dayfrom, $dayto) = split ("-", $points[0]);
  227. #Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: dayfrom $dayfrom dayto $dayto");
  228. my $rulematch = 0;
  229. if ($dayfrom <= $dayto ) { # rule 1-5 or 4-4
  230. $rulematch = ($wday >= $dayfrom && $wday <= $dayto);
  231. } else { # rule 5-2
  232. $rulematch = ($wday >= $dayfrom || $wday <= $dayto);
  233. }
  234. if ($rulematch) {
  235. for (my $i=int(@points)-1; $i>0; $i--) {
  236. Log3 ($hash, 5, "PWMR_CalcDesiredTemp $name: i:$i $points[$i]");
  237. my ($ruletime, $tempV) = split (",", $points[$i]);
  238. if ($cmptime >= $ruletime) {
  239. my $temperature = $hash->{"c_tempN"};
  240. $temperature = $hash->{"c_tempD"} if ($tempV eq "D");
  241. $temperature = $hash->{"c_tempC"} if ($tempV eq "C");
  242. $temperature = $hash->{"c_tempE"} if ($tempV eq "E");
  243. Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: match i:$i $points[$i] ($tempV/$temperature)");
  244. if ($hash->{READINGS}{"desired-temp"}{VAL} != $temperature
  245. or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) {
  246. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $temperature), 1);
  247. } else {
  248. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $temperature), 0);
  249. }
  250. #$hash->{READINGS}{"desired-temp"}{TIME} = TimeNow();
  251. #$hash->{READINGS}{"desired-temp"}{VAL} = $temperature;
  252. #push @{$hash->{CHANGED}}, "desired-temp $temperature";
  253. #DoTrigger($name, undef);
  254. return undef;
  255. }
  256. }
  257. # no interval matched .. guess I am before the first one
  258. # so I choose the temperature from yesterday :-)
  259. # this should be the tempN
  260. my $newTemp = $hash->{"c_tempN"};
  261. my $act_dtemp = $hash->{READINGS}{"desired-temp"}{VAL};
  262. Log3 ($hash, 4, "PWMR_CalcDesiredTemp $name: use last value ($act_dtemp)");
  263. if ($act_dtemp ne $newTemp
  264. or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) {
  265. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $newTemp), 1);
  266. #} else {
  267. # readingsSingleUpdate ($hash, "desired-temp", $newTemp, 0);
  268. }
  269. #$hash->{READINGS}{"desired-temp"}{TIME} = TimeNow();
  270. #$hash->{READINGS}{"desired-temp"}{VAL} = $newTemp;
  271. #push @{$hash->{CHANGED}}, "desired-temp $newTemp";
  272. #DoTrigger($name, undef);
  273. return undef;
  274. }
  275. }
  276. }
  277. } else { # $hash->{c_desiredTempFrom} is set
  278. #$hash->{STATE} = "From $hash->{d_name}";
  279. readingsSingleUpdate ($hash, "state", "From $hash->{d_name}", 1);
  280. my $newTemp = PWMR_getDesiredTempFrom ($hash, $defs{$hash->{d_name}}, $hash->{d_reading}, $hash->{d_regexpTemp});
  281. if ($hash->{READINGS}{"desired-temp"}{VAL} != $newTemp
  282. or substr(TimeNow(),1,8) ne substr($hash->{READINGS}{"desired-temp"}{TIME},1,8)) {
  283. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%.01f", $newTemp), 1);
  284. } else {
  285. readingsSingleUpdate ($hash, "desired-temp", sprintf ("%0.1f", $newTemp), 0);
  286. }
  287. }
  288. } else {
  289. #$hash->{STATE} = "Manual";
  290. readingsSingleUpdate ($hash, "state", "Manual", 1);
  291. }
  292. #DoTrigger($name, undef);
  293. return undef;
  294. }
  295. ###################################
  296. sub
  297. PWMR_Get($@)
  298. {
  299. my ($hash, @a) = @_;
  300. my $u = "Unknown argument $a[1], choose one of previousTemps";
  301. return $u if ($a[1] eq "?");
  302. return "argument is missing" if(int(@a) != 2);
  303. if ($a[1] eq "previousTemps") {
  304. my $msg = "";
  305. $msg .= "IBuffer: ".join (" ", @{$hash->{helper}{PID_I_previousTemps}})."\n" if (defined ($hash->{helper}{PID_I_previousTemps}));
  306. $msg .= "DBuffer: ".join (" ", @{$hash->{helper}{PID_D_previousTemps}})."\n" if (defined ($hash->{helper}{PID_D_previousTemps}));
  307. return $msg
  308. }
  309. if($a[1] ne "status") {
  310. return "unknown get value, valid is status";
  311. }
  312. $hash->{LOCAL} = 1;
  313. RemoveInternalTimer($hash);
  314. my $v = PWMR_CalcDesiredTemp($hash);
  315. delete $hash->{LOCAL};
  316. return "$a[0] $a[1] => recalculatd";
  317. }
  318. #############################
  319. sub
  320. PWMR_Set($@)
  321. {
  322. my ($hash, @a) = @_;
  323. my $name = $hash->{NAME};
  324. my $desiredTempString = "";
  325. if (defined($hash->{READINGS}{"desired-temp"}{VAL})
  326. and ($hash->{READINGS}{"desired-temp"}{VAL} < 6 or $hash->{READINGS}{"desired-temp"}{VAL} > 30)) {
  327. $desiredTempString = $hash->{READINGS}{"desired-temp"}{VAL}.",";
  328. }
  329. my @list = map { ($_.".0", $_+0.5) } (6..29);
  330. my $valList = join (",", @list);
  331. $valList .= ",30.0";
  332. #my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:knob,min:6,max:26,step:0.5,linecap:round interval manualTempDuration:slider,60,60,600";
  333. #my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:uzsuDropDown:$valList interval manualTempDuration:slider,60,60,600";
  334. my $u = "Unknown argument $a[1], choose one of factor actor:off,on desired-temp:$desiredTempString$valList interval manualTempDuration:slider,60,60,600 frostProtect:off,on";
  335. $valList = "slider,6,0.5,30,0.5";
  336. return $u if ($a[1] eq "?");
  337. return $u if(int(@a) < 3);
  338. my $cmd = $a[1];
  339. ##############
  340. # manualTempDuration
  341. if ( $cmd eq "manualTempDuration" ) {
  342. readingsSingleUpdate ($hash, "manualTempDuration", $a[2], 1);
  343. #$hash->{READINGS}{"manualTempDuration"}{VAL} = $a[2];
  344. #$hash->{READINGS}{"manualTempDuration"}{TIME} = TimeNow();
  345. return undef;
  346. }
  347. ##############
  348. # desired-temp
  349. if ( $cmd eq "desired-temp" ) {
  350. my $val = $a[2];
  351. if ( $val < 0 || $val > 30 ) {
  352. return "Unknown argument for $cmd, choose <6..30>";
  353. }
  354. my $duration = defined($hash->{READINGS}{"manualTempDuration"}{VAL}) ? $hash->{READINGS}{"manualTempDuration"}{VAL} * 60 : 60 * 60;
  355. if (defined($a[3])) {
  356. $duration = int($a[3]) * 60;
  357. }
  358. # manual set desired-temp will be set for 1 hour (default)
  359. # afterwards it will be overwritten by auto calc
  360. my $now = time();
  361. readingsBeginUpdate ($hash);
  362. readingsBulkUpdate ($hash, "desired-temp", sprintf ("%.01f", $a[2]));
  363. if ($hash->{c_autoCalcTemp} == 0) {
  364. #$hash->{STATE} = "Manual";
  365. readingsBulkUpdate ($hash, "state", "Manual");
  366. } else {
  367. #$hash->{STATE} = "ManualSetUntil ".FmtTime($now + $duration);
  368. readingsBulkUpdate ($hash, "state", "ManualSetUntil ".FmtTime($now + $duration));
  369. readingsBulkUpdate ($hash, "desired-temp-until", FmtDateTime($now + $duration));
  370. }
  371. readingsEndUpdate($hash, 1);
  372. #readingsSingleUpdate ($hash, "desired-temp", $a[2], 1);
  373. #$hash->{READINGS}{$cmd}{TIME} = FmtDateTime($now + $duration);
  374. #$hash->{READINGS}{$cmd}{VAL} = $val;
  375. #push @{$hash->{CHANGED}}, "$cmd: $val";
  376. #DoTrigger($hash, undef);
  377. return undef
  378. }
  379. ##############
  380. # actor
  381. if ( $cmd eq "actor" ) {
  382. my $val = $a[2];
  383. if ( $val eq "on" || $val eq "off" ) {
  384. PWMR_SetRoom($hash, $val);
  385. return undef;
  386. } else {
  387. return "Unknow argument for $cmd, choose on|off";
  388. }
  389. }
  390. ##############
  391. # frostProtect
  392. if ( $cmd eq "frostProtect" ) {
  393. my $val = $a[2];
  394. if ( $val eq "on" ) {
  395. $hash->{c_frostProtect} = 1;
  396. $attr{$name}{frostProtect} = 1;
  397. return undef;
  398. } elsif ( $val eq "off" ) {
  399. $hash->{c_frostProtect} = 0;
  400. $attr{$name}{frostProtect} = 0;
  401. return undef;
  402. } else {
  403. return "Unknow argument for $cmd, choose on|off";
  404. }
  405. }
  406. ##############
  407. # others
  408. if ($cmd =~ /^interval$|^factor$/) {
  409. my $var = uc($a[1]);
  410. $hash->{$var} = $a[2];
  411. } else {
  412. return $u;
  413. }
  414. return undef;
  415. }
  416. #############################
  417. sub
  418. PWMR_Define($$)
  419. {
  420. my ($hash, $def) = @_;
  421. my @a = split("[ \t][ \t]*", $def);
  422. my $name = $hash->{NAME};
  423. return "syntax: define <name> PWMR <IODev> <factor[,offset]> <tsensor[:reading[:t_regexp]]> <actor>[:<a_regexp_on>] [<window|dummy>[,<window>][:<w_regexp>]] ".
  424. "[<usePID=0>]|".
  425. "[<usePID=1>:<PFactor>:<IFactor>[,<ILookBackCnt>]:<DFactor>[,<DLookBackCnt>]]|".
  426. "[<usePID=2>:<PFactor>:<IFactor>:<DFactor>[,<DLookBackCnt>]]"
  427. if(int(@a) < 6 || int(@a) > 9);
  428. my $iodevname = $a[2];
  429. my $factor = ((int(@a) > 2) ? $a[3] : 0.8);
  430. my $tsensor = ((int(@a) > 3) ? $a[4] : "");
  431. my $actor = ((int(@a) > 4) ? $a[5] : "");
  432. my $window = ((int(@a) > 6) ? $a[6] : "");
  433. my $pid = ((int(@a) > 7) ? $a[7] : "");
  434. $hash->{TEMPSENSOR} = $tsensor;
  435. $hash->{ACTOR} = $actor;
  436. $hash->{WINDOW} = ($window eq "dummy" ? "" : $window);
  437. # definitions used in the past moved to c_factor and c_foffset
  438. delete ($hash->{FACTOR}) if (defined (($hash->{FACTOR})));
  439. delete ($hash->{FOFFSET}) if (defined (($hash->{FOFFSET})));
  440. $hash->{c_desiredTempFrom} = "";
  441. $hash->{p_factor} = $factor;
  442. $hash->{p_tsensor} = $tsensor;
  443. $hash->{p_actor} = $actor;
  444. $hash->{p_window} = $window;
  445. $hash->{p_pid} = $pid;
  446. #$hash->{helper}{cycletime} = 0;
  447. if ( !$iodevname ) {
  448. return "unknown device $iodevname";
  449. }
  450. if ( $defs{$iodevname}->{TYPE} ne "PWM" ) {
  451. return "wrong type of $iodevname (not PWM)";
  452. }
  453. #$hash->{IODev} = $iodev;
  454. $hash->{IODev} = $defs{$iodevname};
  455. ##########
  456. # check window
  457. $hash->{windows} = "";
  458. my ($allwindows, $w_regexp) = split (":", $window, 2);
  459. if ( !defined($w_regexp) )
  460. {
  461. # this regexp defines the result of ReadRoom
  462. # if any window is open return 1
  463. $w_regexp = '.*Open.*'
  464. }
  465. $hash->{w_regexp} = $w_regexp;
  466. if ( defined ($allwindows) ) {
  467. my (@windows) = split (",", $allwindows);
  468. foreach my $onewindow (@windows) {
  469. if (!$defs{$onewindow} && $onewindow ne "dummy") {
  470. my $msg = "$name: Unknown window device $onewindow specified";
  471. Log3 ($hash, 3, "PWMR_Define $msg");
  472. return $msg;
  473. }
  474. if (length($hash->{windows}) > 0 ) {
  475. $hash->{windows} .= ",$onewindow"
  476. } else {
  477. $hash->{windows} = "$onewindow"
  478. }
  479. }
  480. }
  481. ##########
  482. # check pid definition
  483. my ($usePID, $PFactor, $IFactorTmp, $DFactorTmp) = split (":", $pid);
  484. $IFactorTmp = "" unless (defined ($IFactorTmp));
  485. $DFactorTmp = "" unless (defined ($DFactorTmp));
  486. my ($IFactor, $ILookBackCnt) = split (",", $IFactorTmp);
  487. my ($DFactor, $DLookBackCnt) = split (",", $DFactorTmp);
  488. $hash->{c_PID_useit} = !defined($usePID) ? 0 : $usePID;
  489. if ($hash->{c_PID_useit} eq 0) {
  490. # simple p-factor calculation will be done
  491. delete ($hash->{READINGS}{PID_PVal}) if (defined($hash->{READINGS}{PID_PVal}));
  492. delete ($hash->{READINGS}{PID_IVal}) if (defined($hash->{READINGS}{PID_IVal}));
  493. delete ($hash->{READINGS}{PID_DVal}) if (defined($hash->{READINGS}{PID_DVal}));
  494. delete ($hash->{READINGS}{PID_PWMPulse}) if (defined($hash->{READINGS}{PID_PWMPulse}));
  495. delete ($hash->{READINGS}{PID_PWMOnTime}) if (defined($hash->{READINGS}{PID_PWMOnTime}));
  496. delete ($hash->{helper}{PID_I_previousTemps}) if (defined (($hash->{helper}{PID_I_previousTemps})));
  497. delete ($hash->{helper}{PID_D_previousTemps}) if (defined (($hash->{helper}{PID_D_previousTemps})));
  498. delete ($hash->{h_PID_I_previousTemps}) if (defined (($hash->{h_PID_I_previousTemps})));
  499. delete ($hash->{h_PID_D_previousTemps}) if (defined (($hash->{h_PID_D_previousTemps})));
  500. delete ($hash->{c_PID_PFactor}) if (defined (($hash->{c_PID_PFactor})));
  501. delete ($hash->{c_PID_IFactor}) if (defined (($hash->{c_PID_IFactor})));
  502. delete ($hash->{c_PID_DFactor}) if (defined (($hash->{c_PID_DFactor})));
  503. delete ($hash->{c_PID_ILookBackCnt}) if (defined (($hash->{c_PID_ILookBackCnt})));
  504. delete ($hash->{c_PID_DLookBackCnt}) if (defined (($hash->{c_PID_DLookBackCnt})));
  505. delete ($hash->{h_deltaTemp}) if (defined($hash->{h_deltaTemp}));
  506. delete ($hash->{h_deltaTemp_D}) if (defined($hash->{h_deltaTemp_D}));
  507. my ($f, $o) = split (",", $factor);
  508. $f = 1 unless (defined ($f));
  509. $o = 0.11 unless (defined ($o)); # if cycletime is 900 then this increases the on-time by 1:39 (=99 seconds)
  510. $hash->{c_factor} = $f; # pulse is calculated using the below formular
  511. $hash->{c_foffset} = $o; # ( $deltaTemp * $c_factor) ** 2) + $c_foffset
  512. } elsif ($hash->{c_PID_useit} eq 1) {
  513. delete ($hash->{READINGS}{PWMPulse}) if (defined($hash->{READINGS}{PWMPulse}));
  514. delete ($hash->{READINGS}{PWMOnTime}) if (defined($hash->{READINGS}{PWMOnTime}));
  515. delete ($hash->{h_PID_I_previousTemps}) if (defined (($hash->{h_PID_I_previousTemps})));
  516. delete ($hash->{h_PID_D_previousTemps}) if (defined (($hash->{h_PID_D_previousTemps})));
  517. delete ($hash->{c_factor}) if (defined (($hash->{c_factor})));
  518. delete ($hash->{c_foffset}) if (defined (($hash->{c_foffset})));
  519. $hash->{c_PID_PFactor} = !defined($PFactor) ? 0.8 : $PFactor;
  520. $hash->{c_PID_IFactor} = !defined($IFactor) ? 0.3 : $IFactor;
  521. $hash->{c_PID_DFactor} = !defined($DFactor) ? 0.5 : $DFactor;
  522. $hash->{c_PID_ILookBackCnt} = !defined($ILookBackCnt) ? 5 : $ILookBackCnt;
  523. $hash->{c_PID_DLookBackCnt} = !defined($DLookBackCnt) ? 10 : $DLookBackCnt;
  524. $hash->{h_deltaTemp} = 0 unless defined ($hash->{h_deltaTemp});
  525. $hash->{h_deltaTemp_D} = 0 unless defined ($hash->{h_deltaTemp_D});
  526. ### I-Factor
  527. # initialize if not yet done
  528. $hash->{helper}{PID_I_previousTemps} = [] unless defined (($hash->{helper}{PID_I_previousTemps}));
  529. # shorter reference to array
  530. my $IBuffer = $hash->{helper}{PID_I_previousTemps};
  531. my $Icnt = ( @{$IBuffer} ); # or scalar @{$IBuffer}
  532. # reference
  533. #Log3 ($hash, 3, "org reference IBuffer is $hash->{helper}{PID_I_previousTemps} short is $IBuffer, cnt is ". scalar @{$IBuffer}." (starting from 0)");
  534. Log3 ($hash, 4, "content of IBuffer is @{$IBuffer}");
  535. # cut Buffer if it is too large
  536. while (scalar @{$IBuffer} > $hash->{c_PID_ILookBackCnt}) {
  537. my $v = shift @{$IBuffer};
  538. }
  539. ### D-Factor
  540. # initialize if not yet done
  541. $hash->{helper}{PID_D_previousTemps} = [] unless defined (($hash->{helper}{PID_D_previousTemps}));
  542. # shorter reference to array
  543. my $DBuffer = $hash->{helper}{PID_D_previousTemps};
  544. my $Dcnt = ( @{$DBuffer} ); # or scalar @{$DBuffer}
  545. # reference
  546. #Log3 ($hash, 3, "org reference DBuffer is $hash->{helper}{PID_D_previousTemps} short is $DBuffer, cnt is ". scalar @{$DBuffer}." (starting from 0)");
  547. Log3 ($hash, 4, "content of DBuffer is @{$DBuffer}");
  548. # cut Buffer if it is too large
  549. while (scalar @{$DBuffer} > $hash->{c_PID_DLookBackCnt}) {
  550. my $v = shift @{$DBuffer};
  551. }
  552. } else {
  553. # usePID >= 2
  554. delete ($hash->{READINGS}{PWMPulse}) if (defined($hash->{READINGS}{PWMPulse}));
  555. delete ($hash->{READINGS}{PWMOnTime}) if (defined($hash->{READINGS}{PWMOnTime}));
  556. delete ($hash->{h_PID_I_previousTemps}) if (defined (($hash->{h_PID_I_previousTemps})));
  557. delete ($hash->{h_PID_D_previousTemps}) if (defined (($hash->{h_PID_D_previousTemps})));
  558. delete ($hash->{c_factor}) if (defined (($hash->{c_factor})));
  559. delete ($hash->{c_foffset}) if (defined (($hash->{c_foffset})));
  560. $hash->{c_PID_PFactor} = !defined($PFactor) ? 0.8 : $PFactor;
  561. $hash->{c_PID_IFactor} = !defined($IFactor) ? 0.01 : $IFactor;
  562. $hash->{c_PID_DFactor} = !defined($DFactor) ? 0 : $DFactor;
  563. $hash->{c_PID_DLookBackCnt} = !defined($DLookBackCnt) ? 10 : $DLookBackCnt;
  564. delete ($hash->{helper}{PID_I_previousTemps}) if (defined (($hash->{helper}{PID_I_previousTemps})));
  565. #delete ($hash->{helper}{PID_D_previousTemps}) if (defined (($hash->{helper}{PID_D_previousTemps})));
  566. delete ($hash->{c_PID_ILookBackCnt}) if (defined ($hash->{c_PID_ILookBackCnt}));
  567. #delete ($hash->{c_PID_DLookBackCnt}) if (defined ($hash->{c_PID_DLookBackCnt}));
  568. #delete ($hash->{h_deltaTemp}) if (defined ($hash->{h_deltaTemp}));
  569. #delete ($hash->{h_deltaTemp_D}) if (defined ($hash->{h_deltaTemp_D}));
  570. ### D-Factor
  571. # initialize if not yet done
  572. $hash->{helper}{PID_D_previousTemps} = [] unless defined (($hash->{helper}{PID_D_previousTemps}));
  573. # shorter reference to array
  574. my $DBuffer = $hash->{helper}{PID_D_previousTemps};
  575. my $Dcnt = ( @{$DBuffer} ); # or scalar @{$DBuffer}
  576. # reference
  577. #Log3 ($hash, 3, "org reference DBuffer is $hash->{helper}{PID_D_previousTemps} short is $DBuffer, cnt is ". scalar @{$DBuffer}." (starting from 0)");
  578. Log3 ($hash, 4, "content of DBuffer is @{$DBuffer}");
  579. # cut Buffer if it is too large
  580. while (scalar @{$DBuffer} > $hash->{c_PID_DLookBackCnt}) {
  581. my $v = shift @{$DBuffer};
  582. }
  583. }
  584. ##########
  585. # check sensor
  586. # dummy is allowed and will be ignored
  587. my ($sensor, $reading, $t_regexp) = split (":", $tsensor, 3);
  588. if (!$defs{$sensor} && $sensor ne "dummy")
  589. {
  590. my $msg = "$name: Unknown sensor device $sensor specified";
  591. Log3 ($hash, 3, "PWMR_Define $msg");
  592. return $msg;
  593. }
  594. $sensor =~ s/dummy//;
  595. $hash->{t_sensor} = $sensor;
  596. $reading = "temperature" unless (defined($reading));
  597. $hash->{t_reading} = $reading;
  598. if ( !defined($t_regexp) )
  599. {
  600. $t_regexp = '([\\d\\.]+)'
  601. }
  602. $hash->{t_regexp} = $t_regexp;
  603. ##########
  604. # check actor
  605. my ($tactor, $a_regexp_on) = split (":", $actor, 2);
  606. $a_regexp_on = "on" unless defined ($a_regexp_on);
  607. $tactor =~ s/dummy//;
  608. if (!$defs{$tactor} && $tactor ne "dummy")
  609. {
  610. my $msg = "$name: Unknown actor device $tactor specified";
  611. Log3 ($hash, 3, "PWMR_Define $msg");
  612. return $msg;
  613. }
  614. $hash->{actor} = $tactor;
  615. $hash->{a_regexp_on} = $a_regexp_on;
  616. #$hash->{actorState} = "unknown";
  617. readingsSingleUpdate ($hash, "actorState", "unknown", 0);
  618. #$hash->{STATE} = "Initialized";
  619. readingsSingleUpdate ($hash, "state", "Initialized", 1);
  620. # values for calculation of desired-temp
  621. $hash->{c_frostProtect} = 0;
  622. $hash->{c_autoCalcTemp} = 1;
  623. $hash->{c_tempFrostProtect} = 6;
  624. $hash->{c_tempN} = 16;
  625. $hash->{c_tempD} = 20;
  626. $hash->{c_tempC} = 22;
  627. $hash->{c_tempE} = 19;
  628. $hash->{c_tempRule1} = "1-5 0600,D 2200,N";
  629. $hash->{c_tempRule2} = "6-0 0800,D 2200,N";
  630. $hash->{c_tempRule3} = "";
  631. $hash->{c_tempRule4} = "";
  632. $hash->{c_tempRule5} = "";
  633. $hash->{INTERVAL} = 300;
  634. #AssignIoPort($hash);
  635. # if attributes already defined then recall set for them
  636. foreach my $oneattr (sort keys %{$attr{$name}})
  637. {
  638. PWMR_Attr ("set", $name, $oneattr, $attr{$name}{$oneattr});
  639. }
  640. if($hash->{INTERVAL}) {
  641. InternalTimer(gettimeofday()+10, "PWMR_CalcDesiredTemp", $hash, 0);
  642. }
  643. return undef;
  644. }
  645. #############################
  646. sub PWMR_Undef($$)
  647. {
  648. my ($hash, $args) = @_;
  649. my $name = $hash->{NAME};
  650. Log3 ($hash, 3, "PWMR Undef $name");
  651. if ( $hash->{INTERVAL} )
  652. {
  653. RemoveInternalTimer($hash);
  654. }
  655. return undef;
  656. }
  657. #############################
  658. sub
  659. PWMR_SetRoom(@)
  660. {
  661. my ($room, $newState) = @_;
  662. my $name = $room->{NAME};
  663. Log3 ($room, 4, "PWMR_SetRoom $name <$newState>");
  664. my $energyused = "";
  665. if (defined($room->{READINGS}{energyused}{VAL})) {
  666. $energyused = substr ( $room->{READINGS}{energyused}{VAL}, -29);
  667. }
  668. # newState may be "", "on", "off"
  669. if ($newState eq "") {
  670. $energyused = $energyused.substr ( $energyused ,-1);
  671. } else {
  672. $energyused = $energyused.($newState eq "on" ? "1" : "0");
  673. }
  674. readingsBeginUpdate ($room);
  675. readingsBulkUpdate ($room, "energyused", $energyused);
  676. readingsBulkUpdate ($room, "energyusedp", sprintf ("%.1f", ($energyused =~ tr/1//) /30*100));
  677. if ($newState eq "") {
  678. readingsEndUpdate($room, 1);
  679. return;
  680. }
  681. if ($room->{actor})
  682. {
  683. my $ret = fhem sprintf ("set %s %s", $room->{actor}, $newState);
  684. if (!defined($ret)) { # sucessfull
  685. Log3 ($room, 2, "PWMR_SetRoom $room->{NAME}: set $room->{actor} $newState");
  686. #$room->{actorState} = $newState;
  687. readingsBulkUpdate ($room, "actorState", $newState);
  688. readingsBulkUpdate ($room, "lastswitch", time());
  689. readingsEndUpdate($room, 1);
  690. push @{$room->{CHANGED}}, "actor $newState";
  691. DoTrigger($name, undef);
  692. } else {
  693. Log3 ($room, 2, "PWMR_SetRoom $name: set $room->{actor} $newState failed ($ret)");
  694. }
  695. }
  696. }
  697. ###################################
  698. sub
  699. PWMR_ReadRoom(@)
  700. {
  701. my ($room, $cycletime, $MaxPulse) = @_; # room, cylcetime for PMW Calculation (15Min), Max Time to stay on (0.00 .. 1.00)
  702. my $name = $room->{NAME};
  703. my $temperaturT;
  704. my $desiredTemp;
  705. my $prevswitchtimeT;
  706. #$room->{helper}{cycletime} = $cycletime;
  707. my ($temperaturV, $actorV, $factor, $oldpulse, $newpulse, $newpulsePID, $prevswitchtime, $windowV) =
  708. (99, "off", 0, 0, 0, 0, 0, 0);
  709. #Log3 ($room, 4, "PWMR_ReadRoom $name <$room->{t_sensor}> <$room->{actor}>");
  710. if ($room->{t_sensor})
  711. {
  712. my $sensor = $room->{t_sensor};
  713. my $reading = $room->{t_reading};
  714. my $t_regexp = $room->{t_regexp};
  715. $temperaturV = $defs{$sensor}->{READINGS}{$reading}{VAL};
  716. $temperaturT = $defs{$sensor}->{READINGS}{$reading}{TIME};
  717. $temperaturV =~ s/$t_regexp/$1/;
  718. $temperaturV = PWMR_valueFormat ($room, "temperature", $temperaturV);
  719. }
  720. if ($room->{actor})
  721. {
  722. # HERE
  723. #$actorV = (($defs{$room->{actor}}->{STATE} eq "on") : "on" ? "off");
  724. #$actorV = $defs{$room->{actor}}->{STATE};
  725. # until 26.01.2013 -> may be undef which forces room to be switched off first
  726. #$actorV = $room->{actorState};
  727. # starting from 26.01.2013 -> try to read act status .. (may also be invalid if struct)
  728. if ($defs{$room->{actor}}->{TYPE} eq "RBRelais") {
  729. $actorV = $defs{$room->{actor}}->{STATE};
  730. } elsif (defined($defs{$room->{actor}}->{STATE})) {
  731. $actorV = $defs{$room->{actor}}->{STATE};
  732. } else {
  733. #$actorV = $room->{actorState};
  734. $actorV = $room->{READINGS}{actorState};
  735. }
  736. #my $actorVOrg = $actorV;
  737. my $a_regexp_on = $room->{a_regexp_on};
  738. if ($actorV =~ /^$a_regexp_on$/) {
  739. $actorV = "on";
  740. } else {
  741. $actorV = "off";
  742. }
  743. #Log3 ($room, 2, "$name actorV $actorV org($actorVOrg) regexp($a_regexp_on)");
  744. }
  745. if (!$room->{READINGS}{"desired-temp"}{TIME})
  746. {
  747. readingsSingleUpdate ($room, "desired-temp", 6.0, 0);
  748. }
  749. if (!$room->{READINGS}{oldpulse}{TIME})
  750. {
  751. readingsSingleUpdate ($room, "oldpulse", 0.0, 0);
  752. }
  753. if (!$room->{READINGS}{lastswitch}{TIME})
  754. {
  755. readingsSingleUpdate ($room, "lastswitch", time(), 0);
  756. }
  757. $oldpulse = $room->{READINGS}{oldpulse}{VAL};
  758. $prevswitchtime = $room->{READINGS}{lastswitch}{VAL};
  759. $prevswitchtimeT = $room->{READINGS}{lastswitch}{TIME};
  760. $windowV = 0;
  761. if ($room->{windows} && $room->{windows} ne "" && $room->{w_regexp} ne "")
  762. {
  763. foreach my $window (split (",", $room->{windows})) {
  764. Log3 ($room, 4, "PWMR_ReadRoom $name: check window $window");
  765. if (defined($room->{w_regexp}) && $room->{w_regexp} ne "") {
  766. if (defined($defs{$window}) && $defs{$window}{STATE} ) {
  767. Log3 ($room, 5, "PWMR_ReadRoom $name: $window ($defs{$window}{STATE}/$room->{w_regexp})");
  768. if ( $defs{$window}{STATE} =~ /$room->{w_regexp}/ ) {
  769. $windowV = 1;
  770. Log3 ($room, 3, "PWMR_ReadRoom $name: $window state: set to 1");
  771. }
  772. }
  773. }
  774. }
  775. }
  776. if ($windowV > 0) {
  777. $desiredTemp = $room->{c_tempFrostProtect};
  778. } else {
  779. $desiredTemp = $room->{READINGS}{"desired-temp"}{VAL};
  780. }
  781. readingsBeginUpdate ($room);
  782. if ($room->{c_PID_useit} eq 0) {
  783. # simple P-Factor calculation
  784. my $deltaTemp = maxNum (0, $desiredTemp - $temperaturV);
  785. $factor = $room->{c_factor};
  786. my $factoroffset = $room->{c_foffset};
  787. $newpulse = minNum ($MaxPulse, (( $deltaTemp * $factor) ** 2) + $factoroffset); # default 85% max ontime
  788. $newpulse = sprintf ("%.2f", $newpulse);
  789. my $PWMPulse = $newpulse * 100;
  790. my $PWMOnTime = sprintf ("%02s:%02s", int ($newpulse * $cycletime / 60), ($newpulse * $cycletime) % 60);
  791. my $iodev = $room->{IODev};
  792. if ($newpulse * $iodev->{CYCLETIME} < $iodev->{MINONOFFTIME}) {
  793. $PWMPulse = 0;
  794. $PWMOnTime = "00:00";
  795. }
  796. readingsBulkUpdate ($room, "desired-temp-used", $desiredTemp);
  797. readingsBulkUpdate ($room, "PWMOnTime", $PWMOnTime);
  798. readingsBulkUpdate ($room, "PWMPulse", $PWMPulse);
  799. readingsBulkUpdate ($room, "temperature", $temperaturV);
  800. Log3 ($room, 4, "PWMR_ReadRoom $name: desT($desiredTemp), actT($temperaturV von($temperaturT)), state($actorV)");
  801. Log3 ($room, 4, "PWMR_ReadRoom $name: newpulse($newpulse/$PWMOnTime), oldpulse($oldpulse), lastSW($prevswitchtime = $prevswitchtimeT), window($windowV)");
  802. } elsif ($room->{c_PID_useit} eq 1) {
  803. ### PID calculation
  804. my $DBuffer = $room->{helper}{PID_D_previousTemps};
  805. push @{$DBuffer}, $temperaturV;
  806. my $IBuffer = $room->{helper}{PID_I_previousTemps};
  807. push @{$IBuffer}, $temperaturV;
  808. # cut I-Buffer if it is too large
  809. while (scalar @{$IBuffer} > $room->{c_PID_ILookBackCnt}) {
  810. my $v = shift @{$IBuffer};
  811. #Log3 ($room, 3, "shift $v from IBuffer");
  812. }
  813. #Log3 ($room, 3, "IBuffer contains ".scalar @{$IBuffer}." elements");
  814. # cut D-Buffer if it is too large
  815. while (scalar @{$DBuffer} > $room->{c_PID_DLookBackCnt}) {
  816. my $v = shift @{$DBuffer};
  817. #Log3 ($room, 3, "shift $v from DBuffer");
  818. }
  819. #Log3 ($room, 3, "DBuffer contains ".scalar @{$DBuffer}." elements");
  820. # helper for previousTemps
  821. #$room->{h_PID_I_previousTemps} = join (" ", @{$IBuffer});
  822. #$room->{h_PID_D_previousTemps} = join (" ", @{$DBuffer});
  823. my $deltaTempPID = $desiredTemp - $temperaturV;
  824. $room->{h_deltaTemp} = sprintf ("%.1f", -1 * $deltaTempPID);
  825. $room->{h_deltaTemp_D} = sprintf ("%.1f", -1 * ($desiredTemp - $DBuffer->[0]));
  826. my $ISum = 0;
  827. foreach my $t (@{$IBuffer}) {
  828. $ISum += ($desiredTemp - $t);
  829. }
  830. $ISum = $ISum;
  831. my $PVal = $room->{c_PID_PFactor} * maxNum (0, $deltaTempPID);
  832. my $IVal = $room->{c_PID_IFactor} * $ISum;
  833. my $DVal = $room->{c_PID_DFactor} * ($room->{h_deltaTemp_D} - $room->{h_deltaTemp});
  834. $PVal = minNum (1, sprintf ("%.2f", $PVal));
  835. $IVal = minNum (1, sprintf ("%.2f", $IVal));
  836. $DVal = minNum (1, sprintf ("%.2f", $DVal));
  837. $IVal = maxNum (-1, $IVal);
  838. my $newpulsePID = ($PVal + $IVal + $DVal);
  839. $newpulsePID = minNum ($MaxPulse, sprintf ("%.2f", $newpulsePID));
  840. $newpulsePID = maxNum (0, sprintf ("%.2f", $newpulsePID));
  841. my $PWMPulsePID = $newpulsePID * 100;
  842. my $PWMOnTimePID = sprintf ("%02s:%02s", int ($newpulsePID * $cycletime / 60), ($newpulsePID * $cycletime) % 60);
  843. my $iodev = $room->{IODev};
  844. if ($PWMPulsePID * $iodev->{CYCLETIME} < $iodev->{MINONOFFTIME}) {
  845. $PWMPulsePID = 0;
  846. $PWMOnTimePID = "00:00";
  847. }
  848. readingsBulkUpdate ($room, "desired-temp-used", $desiredTemp);
  849. readingsBulkUpdate ($room, "temperature", $temperaturV);
  850. #readingsBulkUpdate ($room, "PWMOnTime", $PWMOnTimePID);
  851. #readingsBulkUpdate ($room, "PWMPulse", $PWMPulsePID);
  852. readingsBulkUpdate ($room, "PID_PVal", $PVal);
  853. readingsBulkUpdate ($room, "PID_IVal", $IVal);
  854. readingsBulkUpdate ($room, "PID_DVal", $DVal);
  855. readingsBulkUpdate ($room, "PID_PWMPulse", $PWMPulsePID);
  856. readingsBulkUpdate ($room, "PID_PWMOnTime", $PWMOnTimePID);
  857. Log3 ($room, 4, "PWMR_ReadRoom $name: desT($desiredTemp), actT($temperaturV von($temperaturT)), state($actorV)");
  858. Log3 ($room, 4, "PWMR_ReadRoom $name: newpulse($newpulsePID/$PWMOnTimePID), oldpulse($oldpulse), lastSW($prevswitchtime = $prevswitchtimeT), window($windowV)");
  859. $newpulse = $newpulsePID;
  860. } elsif($room->{c_PID_useit} >= 2) {
  861. my $DBuffer = $room->{helper}{PID_D_previousTemps};
  862. push @{$DBuffer}, $temperaturV;
  863. # cut D-Buffer if it is too large
  864. while (scalar @{$DBuffer} > $room->{c_PID_DLookBackCnt}) {
  865. my $v = shift @{$DBuffer};
  866. #Log3 ($room, 3, "shift $v from DBuffer");
  867. }
  868. #Log3 ($room, 3, "DBuffer contains ".scalar @{$DBuffer}." elements");
  869. my $deltaTempPID = $desiredTemp - $temperaturV;
  870. $room->{h_deltaTemp} = sprintf ("%.1f", -1 * $deltaTempPID);
  871. $room->{h_deltaTemp_D} = sprintf ("%.1f", -1 * ($desiredTemp - $DBuffer->[0]));
  872. #calculate IValue
  873. my $ISum = $room->{READINGS}{"PID_IVal"}{VAL};
  874. $ISum = $ISum + ($deltaTempPID * $room->{c_PID_IFactor});
  875. my $PVal = $room->{c_PID_PFactor} * $deltaTempPID;
  876. my $IVal = $ISum;
  877. my $DVal = $room->{c_PID_DFactor} * ($room->{h_deltaTemp_D} - $room->{h_deltaTemp});
  878. $PVal = sprintf ("%.4f", $PVal);
  879. $IVal = minNum (1, sprintf ("%.4f", $IVal));
  880. $DVal = minNum (1, sprintf ("%.4f", $DVal));
  881. $IVal = maxNum (0, $IVal);
  882. my $newpulsePID = ($PVal + $IVal + $DVal);
  883. $newpulsePID = minNum ($MaxPulse, sprintf ("%.2f", $newpulsePID));
  884. $newpulsePID = maxNum (0, sprintf ("%.2f", $newpulsePID));
  885. my $PWMPulsePID = $newpulsePID * 100;
  886. my $PWMOnTimePID = sprintf ("%02s:%02s", int ($newpulsePID * $cycletime / 60), ($newpulsePID * $cycletime) % 60);
  887. my $iodev = $room->{IODev};
  888. if ($PWMPulsePID * $iodev->{CYCLETIME} < $iodev->{MINONOFFTIME}) {
  889. $PWMPulsePID = 0;
  890. $PWMOnTimePID = "00:00";
  891. }
  892. readingsBulkUpdate ($room, "desired-temp-used", $desiredTemp);
  893. readingsBulkUpdate ($room, "temperature", $temperaturV);
  894. #readingsBulkUpdate ($room, "PWMOnTime", $PWMOnTimePID);
  895. #readingsBulkUpdate ($room, "PWMPulse", $PWMPulsePID);
  896. readingsBulkUpdate ($room, "PID_PVal", $PVal);
  897. readingsBulkUpdate ($room, "PID_IVal", $IVal);
  898. readingsBulkUpdate ($room, "PID_DVal", $DVal);
  899. readingsBulkUpdate ($room, "PID_PWMPulse", $PWMPulsePID);
  900. readingsBulkUpdate ($room, "PID_PWMOnTime", $PWMOnTimePID);
  901. Log3 ($room, 4, "PWMR_ReadRoom $name: desT($desiredTemp), actT($temperaturV von($temperaturT)), state($actorV)");
  902. Log3 ($room, 4, "PWMR_ReadRoom $name: newpulse($newpulsePID/$PWMOnTimePID), oldpulse($oldpulse), lastSW($prevswitchtime = $prevswitchtimeT), window($windowV)");
  903. $newpulse = $newpulsePID;
  904. }
  905. readingsEndUpdate($room, 1);
  906. return ($temperaturV, $actorV, $factor, $oldpulse, $newpulse, $prevswitchtime, $windowV);
  907. }
  908. sub
  909. PWMR_normTime ($)
  910. {
  911. my ($time) = @_;
  912. my $hour = 0;
  913. my $minute = 0;
  914. #Log 4, "normTime $time";
  915. $time =~ /^([0-9]+):*([0-9]*)$/;
  916. if (defined ($2) && ($2 ne "")) { # set $minute to 0 if time was only 6
  917. $minute = $2;
  918. }
  919. if (defined ($1)) { # error if no hour given
  920. $hour = $1
  921. } else {
  922. return undef;
  923. }
  924. #Log 4, "<$hour> <$minute>";
  925. if ($hour < 0 || $hour > 23) {
  926. return undef;
  927. }
  928. if ($minute < 0 || $minute > 59) {
  929. return undef;
  930. }
  931. #Log 4, "uhrzeit $hour $minute";
  932. return sprintf ("%02d%02d", $hour, $minute);
  933. }
  934. sub
  935. PWMR_NormalizeRules(@)
  936. {
  937. my ($hash) = @_;
  938. my $name = $hash->{NAME};
  939. my $rule;
  940. my @week = ();
  941. #Log3 ($hash, 2, "PWMR_NormalizeRules");
  942. if ($hash->{c_autoCalcTemp} == 0 or $hash->{c_desiredTempFrom} ne "")
  943. {
  944. #Log3 ($hash, 2, "PWMR_NormalizeRules delete readings timer._..");
  945. delete ($hash->{READINGS}{timer1_Mo}) if (defined ($hash->{READINGS}{timer1_Mo}));
  946. delete ($hash->{READINGS}{timer2_Di}) if (defined ($hash->{READINGS}{timer2_Di}));
  947. delete ($hash->{READINGS}{timer3_Mi}) if (defined ($hash->{READINGS}{timer3_Mi}));
  948. delete ($hash->{READINGS}{timer4_Do}) if (defined ($hash->{READINGS}{timer4_Do}));
  949. delete ($hash->{READINGS}{timer5_Fr}) if (defined ($hash->{READINGS}{timer5_Fr}));
  950. delete ($hash->{READINGS}{timer6_Sa}) if (defined ($hash->{READINGS}{timer6_Sa}));
  951. delete ($hash->{READINGS}{timer7_So}) if (defined ($hash->{READINGS}{timer7_So}));
  952. return;
  953. }
  954. foreach my $var ("c_tempRule1", "c_tempRule2", "c_tempRule3", "c_tempRule4", "c_tempRule5")
  955. {
  956. $rule = $hash->{$var};
  957. if ($rule ne "") { # valid rule is 1-5 0600,D 1800,C 2200,N
  958. Log3 ($hash, 5, "PWMR_NormalizeRules from $var: $rule");
  959. my ($day, @points) = split (" ", $rule);
  960. my ($dayFromNo, $dayToNo) = split ("-", $day);
  961. $dayFromNo = 7 if ($dayFromNo == 0);
  962. $dayToNo = 7 if ($dayToNo == 0);
  963. my $lastTime = "";
  964. my $lastTempId = "";
  965. my $lastTemp = "";
  966. my $ruleLong = "";
  967. foreach my $step (@points) {
  968. $step =~ /^(..)(..),(.)$/;
  969. my ($actTime, $actTempId) = ($1.":".$2, $3);
  970. if ($lastTime ne "") {
  971. $ruleLong .= sprintf ("%s-%s,%s,%s ", $lastTime, $actTime, $lastTempId, $lastTemp);
  972. }
  973. $lastTime = $actTime;
  974. $lastTempId = $actTempId;
  975. if ($actTempId eq "D") {
  976. $lastTemp = $hash->{c_tempD};
  977. } elsif ($actTempId eq "E") {
  978. $lastTemp = $hash->{c_tempE};
  979. } elsif ($actTempId eq "C") {
  980. $lastTemp = $hash->{c_tempC};
  981. } else {
  982. $lastTemp = $hash->{c_tempN};
  983. }
  984. }
  985. if (uc($lastTempId) ne "N") {
  986. $ruleLong .= sprintf "%s-%s,%s,%s ", $lastTime, "23:59", $lastTempId, $lastTemp;
  987. }
  988. Log3 ($hash, 5, "PWMR_NormalizeRules to $var: $dayFromNo-$dayToNo $ruleLong");
  989. for (my $i=1; $i<=7; $i++)
  990. {
  991. # only first rule matches
  992. if ($dayFromNo <= $dayToNo) {
  993. # rule 1 .. 5
  994. if ($i >= $dayFromNo and $i <= $dayToNo) {
  995. $week[$i] = $ruleLong if (!defined($week[$i]));
  996. }
  997. } else {
  998. # rule 7 .. 1
  999. if ($i >= $dayFromNo or $i <= $dayToNo) {
  1000. $week[$i] = $ruleLong if (!defined($week[$i]));
  1001. }
  1002. }
  1003. }
  1004. }
  1005. }
  1006. # update Readings
  1007. readingsBeginUpdate ($hash);
  1008. readingsBulkUpdate ($hash, "timer1_Mo", (defined($week[1]) ? $week[1] : ""));
  1009. readingsBulkUpdate ($hash, "timer2_Di", (defined($week[2]) ? $week[2] : ""));
  1010. readingsBulkUpdate ($hash, "timer3_Mi", (defined($week[3]) ? $week[3] : ""));
  1011. readingsBulkUpdate ($hash, "timer4_Do", (defined($week[4]) ? $week[4] : ""));
  1012. readingsBulkUpdate ($hash, "timer5_Fr", (defined($week[5]) ? $week[5] : ""));
  1013. readingsBulkUpdate ($hash, "timer6_Sa", (defined($week[6]) ? $week[6] : ""));
  1014. readingsBulkUpdate ($hash, "timer7_So", (defined($week[7]) ? $week[7] : ""));
  1015. readingsEndUpdate($hash, 0);
  1016. }
  1017. sub
  1018. PWMR_CheckTempRule(@)
  1019. {
  1020. my ($hash, $attrname, $var, $vals) = @_;
  1021. my $name = $hash->{NAME};
  1022. my $valid = "";
  1023. my $usage = "usage: [Mo|Di|..|So[-Mo|-Di|..|-So] <zeit>,D|C|E|N [<zeit>,D|C|E|N] ]\n".
  1024. "e.g. Mo-Fr 6:00,D 22,N\n".
  1025. "or So 10,D 23,N";
  1026. Log3 ($hash, 4, "PWMR_CheckTempRule: $hash->{NAME} $var <$vals>");
  1027. my ($day, @points) = split (" ", $vals);
  1028. unless ( $day =~ /-/ ) { # normalise Mo to Mo-Mo
  1029. $day = "$day-$day";
  1030. }
  1031. # analyse Mo-Di
  1032. my ($from, $to) = split ("-", $day);
  1033. $from = lc ($from);
  1034. $to = lc ($to);
  1035. if (defined ($dayno{$from}) && defined ($dayno{$to})) {
  1036. $valid .= "$dayno{$from}-$dayno{$to} ";
  1037. } else {
  1038. return $usage;
  1039. }
  1040. Log3 ($hash, 4, "PWMR_CheckTempRule: $name day valid: $valid");
  1041. foreach my $point (@points) {
  1042. #Log3 ($hash, 4, "loop: $point");
  1043. my ($from, $temp) = split(",", $point);
  1044. $temp = uc($temp);
  1045. unless ($temp eq "D" || $temp eq "N" || $temp eq "C" || $temp eq "E") { # valid temp
  1046. return $usage;
  1047. }
  1048. #Log3 ($hash, 4, "loop: fromto: $fromto");
  1049. return $usage unless ( $from = PWMR_normTime($from) );
  1050. Log3 ($hash, 4, "PWMR_CheckTempRule: $name time valid: $from,$temp");
  1051. $valid .= "$from,$temp ";
  1052. }
  1053. Log3 ($hash, 4, "PWMR_CheckTempRule: $name $var <$valid>");
  1054. $hash->{$var} = $valid;
  1055. PWMR_NormalizeRules($hash);
  1056. return undef
  1057. }
  1058. sub
  1059. PWMR_CheckTemp(@)
  1060. {
  1061. my ($hash, $var, $vals) = @_;
  1062. my $error = "valid values are 0 ... 30";
  1063. my $name = $hash->{NAME};
  1064. Log3 ($hash, 4, "PWMR_CheckTemp: $name $var <$vals>");
  1065. if ($vals !~ /^[0-9]+\.{0,1}[0-9]*$/ ) {
  1066. return "$error";
  1067. } else {
  1068. }
  1069. if ($vals < 0 || $vals > 30) {
  1070. return "$error";
  1071. }
  1072. $hash->{$var} = $vals;
  1073. PWMR_NormalizeRules($hash);
  1074. return undef;
  1075. }
  1076. sub
  1077. PWMR_Attr(@)
  1078. {
  1079. #my @a = @_;
  1080. my ($action, $name, $attrname, $attrval) = @_;
  1081. #my $name = $a[1];
  1082. #my $attr = $a[2];
  1083. #my $val = defined ($a[3]) ? $a[3] : "";
  1084. #my $val = $a[3];
  1085. my $hash = $defs{$name};
  1086. $attrval = "" unless defined ($attrval);
  1087. #Log3 ($hash, 2, "Attr cmd: $action, Attr $attrname value <$attrval> attr <$attr{$name}{$attrname}>");
  1088. if ($action eq "del") {
  1089. Log3 ($hash, 4, "PWMR_Attr: $name, delete $attrname");
  1090. if ($attrname eq "tempRule1") {
  1091. $hash->{c_tempRule1} = "";
  1092. PWMR_NormalizeRules($hash);
  1093. } elsif ($attrname eq "tempRule2") {
  1094. $hash->{c_tempRule2} = "";
  1095. PWMR_NormalizeRules($hash);
  1096. } elsif ($attrname eq "tempRule3") {
  1097. $hash->{c_tempRule3} = "";
  1098. PWMR_NormalizeRules($hash);
  1099. } elsif ($attrname eq "tempRule4") {
  1100. $hash->{c_tempRule4} = "";
  1101. PWMR_NormalizeRules($hash);
  1102. } elsif ($attrname eq "tempRule5") {
  1103. $hash->{c_tempRule5} = "";
  1104. PWMR_NormalizeRules($hash);
  1105. } elsif ($attrname eq "frostProtect") {
  1106. $hash->{c_frostProtect} = 0;
  1107. } elsif ($attrname eq "tempFrostProtect") {
  1108. $hash->{c_tempFrostProtect} = 6;
  1109. } elsif ($attrname eq "desiredTempFrom") {
  1110. $hash->{c_desiredTempFrom} = "";
  1111. delete($hash->{d_name});
  1112. delete($hash->{d_reading});
  1113. delete($hash->{d_regexpTemp});
  1114. PWMR_NormalizeRules($hash);
  1115. } elsif ($attrname eq "autoCalcTemp") {
  1116. $hash->{c_autoCalcTemp} = 1;
  1117. #$hash->{STATE} = "Calculating";
  1118. readingsSingleUpdate ($hash, "state", "Calculating", 1);
  1119. PWMR_NormalizeRules($hash);
  1120. }
  1121. if ($attrname eq "valueFormat" and defined ($hash->{helper}{$attrname})) {
  1122. delete ($hash->{helper}{$attrname});
  1123. }
  1124. PWMR_NormalizeRules($hash);
  1125. return undef;
  1126. }
  1127. Log3 ($hash, 4, "PWMR_Attr: $name, $attrname, $attrval");
  1128. if ($attrname eq "frostProtect") { # frostProtect 0/1
  1129. if ($attrval eq 0 or $attrval eq 1) {
  1130. $hash->{c_frostProtect} = $attrval;
  1131. } elsif ($attrval eq "") {
  1132. $hash->{c_frostProtect} = 0;
  1133. } else {
  1134. return "valid values are 0 or 1";
  1135. }
  1136. } elsif ($attrname eq "autoCalcTemp") { # autoCalcTemp 0/1
  1137. if ($attrval eq 0) {
  1138. $hash->{c_autoCalcTemp} = 0;
  1139. #$hash->{STATE} = "Manual";
  1140. readingsSingleUpdate ($hash, "state", "Manual", 1);
  1141. } elsif ( $attrval eq 1) {
  1142. $hash->{c_autoCalcTemp} = 1;
  1143. #$hash->{STATE} = "Calculating";
  1144. readingsSingleUpdate ($hash, "state", "Calculating", 1);
  1145. } elsif ($attrval eq "") {
  1146. $hash->{c_autoCalcTemp} = 1;
  1147. #$hash->{STATE} = "Calculating";
  1148. readingsSingleUpdate ($hash, "state", "Calculating", 1);
  1149. } else {
  1150. return "valid values are 0 or 1";
  1151. }
  1152. PWMR_NormalizeRules($hash);
  1153. } elsif ($attrname eq "desiredTempFrom") { # desiredTempFrom
  1154. $hash->{c_desiredTempFrom} = $attrval;
  1155. my ( $d_name, $d_reading, $d_regexpTemp) = split (":", $attrval, 3);
  1156. # set defaults
  1157. $hash->{d_name} = (defined($d_name) ? $d_name : "");
  1158. $hash->{d_reading} = (defined($d_reading) ? $d_reading : "desired-temp");
  1159. $hash->{d_regexpTemp} = (defined($d_regexpTemp) ? $d_regexpTemp : '(\d[\d\\.]+)');
  1160. # check if device exist
  1161. unless (defined($defs{$hash->{d_name}})) {
  1162. return "error: $hash->{d_name} does not exist.";
  1163. }
  1164. PWMR_NormalizeRules($hash);
  1165. } elsif ($attrname eq "tempDay") { # tempDay
  1166. return PWMR_CheckTemp($hash, "c_tempD", $attrval);
  1167. } elsif ($attrname eq "tempNight") { # tempNight
  1168. return PWMR_CheckTemp($hash, "c_tempN", $attrval);
  1169. } elsif ($attrname eq "tempCosy") { # tempCosy
  1170. return PWMR_CheckTemp($hash, "c_tempC", $attrval);
  1171. } elsif ($attrname eq "tempEnergy") { # tempEnergy
  1172. return PWMR_CheckTemp($hash, "c_tempE", $attrval);
  1173. } elsif ($attrname eq "tempFrostProtect") { # tempFrostProtect
  1174. return PWMR_CheckTemp($hash, "c_tempFrostProtect", $attrval);
  1175. } elsif ($attrname eq "tempRule1") { # tempRule1
  1176. return PWMR_CheckTempRule($hash, $attrname, "c_tempRule1", $attrval);
  1177. } elsif ($attrname eq "tempRule2") { # tempRule2
  1178. return PWMR_CheckTempRule($hash, $attrname, "c_tempRule2", $attrval);
  1179. } elsif ($attrname eq "tempRule3") { # tempRule3
  1180. return PWMR_CheckTempRule($hash, $attrname, "c_tempRule3", $attrval);
  1181. } elsif ($attrname eq "tempRule4") { # tempRule4
  1182. return PWMR_CheckTempRule($hash, $attrname, "c_tempRule4", $attrval);
  1183. } elsif ($attrname eq "tempRule5") { # tempRule5
  1184. return PWMR_CheckTempRule($hash, $attrname, "c_tempRule5", $attrval);
  1185. }
  1186. if ($attrname eq "valueFormat") {
  1187. my $attrValTmp = $attrval;
  1188. if( $attrValTmp =~ m/^{.*}$/s && $attrValTmp =~ m/=>/ && $attrValTmp !~ m/\$/ ) {
  1189. my $av = eval $attrValTmp;
  1190. if( $@ ) {
  1191. Log3 ($hash->{NAME}, 3, $hash->{NAME} ." $attrname: ". $@);
  1192. } else {
  1193. $attrValTmp = $av if( ref($av) eq "HASH" );
  1194. }
  1195. $hash->{helper}{$attrname} = $attrValTmp;
  1196. foreach my $key (keys %{$hash->{helper}{$attrname}}) {
  1197. Log3 ($hash->{NAME}, 3, $hash->{NAME} ." $key ".$hash->{helper}{$attrname}{$key});
  1198. }
  1199. #return "$attrname set to $attrValTmp";
  1200. } else {
  1201. # if valueFormat is not verified sucessfully ... the helper is deleted (=not used)
  1202. delete $hash->{helper}{$attrname};
  1203. }
  1204. return undef;
  1205. }
  1206. return undef;
  1207. }
  1208. sub
  1209. PWMR_Boost(@)
  1210. {
  1211. my ($me, $outsideSensor, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) = @_;
  1212. return undef unless defined ($defs{$me}->{NAME});
  1213. my $room = $defs{$me};
  1214. my $name = $room->{NAME};
  1215. my $outsideTemp = 99;
  1216. if (defined($defs{$outsideSensor}->{READINGS}{temperature}{VAL})) {
  1217. $outsideTemp = $defs{$outsideSensor}->{READINGS}{temperature}{VAL};
  1218. }
  1219. if ($room->{t_sensor})
  1220. {
  1221. my $sensor = $room->{t_sensor};
  1222. my $reading = $room->{t_reading};
  1223. my $t_regexp = $room->{t_regexp};
  1224. my $temperaturV = $defs{$sensor}->{READINGS}{$reading}{VAL};
  1225. $temperaturV =~ s/$t_regexp/$1/;
  1226. my $desiredTemp = $room->{READINGS}{"desired-temp"}{VAL};
  1227. # boost necessary?
  1228. if (($outsideTemp < $outsideMax)
  1229. && ($temperaturV <= $desiredTemp - $deltaTemp)) {
  1230. Log3 ($room, 3, "PWMR_Boost: $name ".
  1231. "($outsideTemp, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) ".
  1232. "temp($temperaturV) desired-temp($desiredTemp) -> boost");
  1233. my $now = time();
  1234. readingsBeginUpdate ($room);
  1235. readingsBulkUpdate ($room, "desired-temp", sprintf ("%.01f", $desiredTemp + $desiredOffset));
  1236. readingsBulkUpdate ($room, "desired-temp-until", FmtDateTime($now + $boostDuration * 60));
  1237. readingsEndUpdate($room, 1);
  1238. #$room->{READINGS}{"desired-temp"}{TIME} = FmtDateTime($now + $boostDuration * 60);
  1239. #$room->{READINGS}{"desired-temp"}{VAL} = $desiredTemp + $desiredOffset;
  1240. #my $t = $room->{READINGS}{"desired-temp"}{VAL};
  1241. #push @{$room->{CHANGED}}, "desired-temp $t";
  1242. #DoTrigger($name, undef);
  1243. Log3 ($room, 4, "PWMR_Boost: $name ".
  1244. "set desired-temp ".$room->{READINGS}{"desired-temp"}{TIME}." for ".
  1245. $room->{READINGS}{"desired-temp"}{VAL});
  1246. } else {
  1247. Log3 ($room, 3, "PWMR_Boost: $name ".
  1248. "($outsideTemp, $outsideMax, $deltaTemp, $desiredOffset, $boostDuration) ".
  1249. "temp($temperaturV) desired-temp($desiredTemp) -> do nothing");
  1250. }
  1251. } else {
  1252. Log3 ($room, 3, "PWMR_Boost: $name warning: no sensor.");
  1253. }
  1254. return undef;
  1255. }
  1256. sub
  1257. PWMR_valueFormat(@)
  1258. {
  1259. my ($hash, $reading, $value) = @_;
  1260. return $value unless (defined ($reading));
  1261. if (ref($hash->{helper}{valueFormat}) eq 'HASH')
  1262. {
  1263. if (exists($hash->{helper}{valueFormat}->{$reading})) {
  1264. my $vf = $hash->{helper}{valueFormat}->{$reading};
  1265. return sprintf ("$vf", $value);
  1266. }
  1267. }
  1268. return $value;
  1269. }
  1270. 1;
  1271. =pod
  1272. =item device
  1273. =item summary Device for room temperature control using PWM. See also 94_PWM.pm
  1274. =begin html
  1275. <a name="PWMR"></a>
  1276. <h3>PWMR</h3>
  1277. <ul>
  1278. <table>
  1279. <tr><td>
  1280. The PMWR module defines rooms to be used for calculation within module PWM.<br><br>
  1281. PWM is based on Pulse Width Modulation which means valve position 70% is implemented in switching the device on for 70% and off for 30% in a given timeframe.<br>
  1282. PWM defines a calculation unit and depents on objects based on PWMR which define the rooms to be heated.<br>
  1283. PWMR objects calculate a desired temperature for a room based on several rules, define windows, a temperature sensor and an actor to be used to switch on/off heating.
  1284. <br>
  1285. </td></tr>
  1286. </table>
  1287. <b>Define</b>
  1288. <ul>
  1289. <code>define &lt;name&gt; PWMR &lt;IODev&gt; &lt;factor[,offset]&gt; &lt;tsensor[:reading:[t_regexp]]&gt; &lt;actor&gt;[:&lt;a_regexp_on&gt;] [&lt;window|dummy&gt;[,&lt;window&gt;[:&lt;w_regexp&gt;]] [ &lt;usePID=0&gt; | &lt;usePID=1&gt;:&lt;PFactor&gt;:&lt;IFactor&gt;[,&lt;ILookBackCnt&gt;]:&lt;DFactor&gt;[,&lt;DLookBackCnt&gt;] | &lt;usePID=2&gt;:&lt;PFactor&gt;:&lt;IFactor&gt;:&lt;DFactor&gt[,&lt;DLookBackCnt&gt;] ] <br></code>
  1290. <br>
  1291. Define a calculation object with the following parameters:<br>
  1292. <ul>
  1293. <li>IODev<br>
  1294. Reference to an object of TYPE PWM. This object will switch on/off heating.<br>
  1295. </li>
  1296. <li>factor[,offset]<br>
  1297. Pulse for PWM will be calculated as ((delta-temp * factor) ** 2) + offset.<br>
  1298. <i>offset</i> defaults to 0.11<br>
  1299. <i>factor</i> can be used to weight rooms.<br>
  1300. </li>
  1301. <li>tsensor[:reading[:t_regexp]]<br>
  1302. <i>tsensor</i> defines the temperature sensor for the actual room temperature.<br>
  1303. <i>reading</i> defines the reading of the temperature sensor. Default is "temperature"<br>
  1304. <i>t_regexp</i> defines a regular expression to be applied to the reading. Default is '(\d[\d\.]+)'.<br>
  1305. </li>
  1306. <li>actor[:&lt;a_regexp_on&gt;]<br>
  1307. The actor will be set to "on" of "off" to turn on/off heating.<br>
  1308. <i>a_regexp_on</i> defines a regular expression to be applied to the state of the actor. Default is 'on". If state matches the regular expression it is handled as "on", otherwise "off"<br>
  1309. </li>
  1310. <li>&lt;window|dummy&gt;[,&lt;window&gt;[:&lt;w_regexp&gt;]<br>
  1311. <i>window</i> defines several window devices that can prevent heating to be turned on.<br>
  1312. If STATE matches the regular expression then the desired-temp will be decreased to frost-protect temperature.<br>
  1313. 'dummy' can be used as a neutral value for window and will be ignored when processing the configuration.<br>
  1314. <i>w_regexp</i> defines a regular expression to be applied to the reading. Default is '.*Open.*'.<br>
  1315. </li>
  1316. <li>
  1317. <code>&lt;usePID=0&gt;</code><br>
  1318. <i>usePID 0</i>: calculate Pulse based on parameters factor and offset.<br>
  1319. Internals c_factor and c_foffset will reflect the values used for calculatio. Defaults are 1 and 0.11 (if not specified)<br>
  1320. Readings PWMOnTime and PWMPulse will reflect the actual calculated Pulse.<br>
  1321. </li>
  1322. <li>
  1323. <code>&lt;usePID=1&gt;:&lt;PFactor&gt;:&lt;IFactor&gt;[,&lt;ILookBackCnt&gt;]:&lt;DFactor&gt;[,&lt;DLookBackCnt&gt;]</code><br>
  1324. <i>PFactor</i>: Konstant for P. Default is 0.8.<br>
  1325. <i>IFactor</i>: Konstant for I. Default is 0.3<br>
  1326. <i>DFactor</i>: Konstant for D. Default is 0.5<br>
  1327. <i>ILookBackCnt</i>: Buffer size to store previous temperatures. For I calculation all values will be used. Default is 5.<br>
  1328. <i>DLookBackCnt</i>: Buffer size to store previous temperatures. For D calculation actual and oldest temperature will be used. Default is 10.<br>
  1329. Internals c_PID_PFactor, c_PID_IFactor, c_PID_ILookBackCnt, c_PID_DFactor, c_PID_DLookBackCnt and c_PID_useit will reflect the above configuration values.<br>
  1330. Readings PID_DVal, PID_IVal, PID_PVal, PID_PWMOnTime and PID_PWMPulse will reflect the actual calculated PID values and Pulse.<br>
  1331. </li>
  1332. <li>
  1333. <code>&lt;usePID=2&gt;:&lt;PFactor&gt;:&lt;IFactor&gt;:&lt;DFactor&gt;[,&lt;DLookBackCnt&gt;]</code><br>
  1334. <i>PFactor</i>: Konstant for P. Default is 0.8.<br>
  1335. <i>IFactor</i>: Konstant for I. Default is 0.01<br>
  1336. <i>DFactor</i>: Konstant for D. Default is 0<br>
  1337. <i>DLookBackCnt</i>: Buffer size to store previous temperatures. For D calculation actual and oldest temperature will be used. Default is 10.<br>
  1338. Internals c_PID_PFactor, c_PID_IFactor, c_PID_DFactor, c_PID_DLookBackCnt and c_PID_useit will reflect the above configuration values.<br>
  1339. Readings PID_DVal, PID_IVal, PID_PVal, PID_PWMOnTime and PID_PWMPulse will reflect the actual calculated PID values and Pulse.<br>
  1340. </li>
  1341. </ul>
  1342. <br>
  1343. Example:<br>
  1344. <br>
  1345. <code>define roomKitchen PWMR fh 1,0.11 tempKitchen relaisKitchen</code><br>
  1346. <code>define roomKitchen PWMR fh 1,0.11 tempKitchen relaisKitchen windowKitchen1,windowKitchen2</code><br>
  1347. <code>define roomKitchen PWMR fh 1,0.11 tempKitchen relaisKitchen windowKitchen1,windowKitchen2:.*Open.*</code><br>
  1348. <code>define roomKitchen PWMR fh 1,0.11 tempKitchen relaisKitchen windowKitchen1,windowKitchen2</code><br>
  1349. <code>define roomKitchen PWMR fh 1,0.11 tempKitchen relaisKitchen dummy 0</code><br>
  1350. <code>define roomKitchen PWMR fh 0 tempKitchen relaisKitchen dummy 1:0.8:0.3:0.5</code><br>
  1351. <code>define roomKitchen PWMR fh 0 tempKitchen relaisKitchen dummy 1:0.8:0.3,5:0.5,10</code><br>
  1352. <code>define roomKitchen PWMR fh 0 tempKitchen relaisKitchen dummy 2:0.8:0.01:00</code><br>
  1353. <code>define roomKitchen PWMR fh 0 tempKitchen relaisKitchen dummy 2:0.8:0.01:0.1,10</code><br>
  1354. <br>
  1355. </ul>
  1356. <br>
  1357. <b>Set </b>
  1358. <ul>
  1359. <li>factor<br>
  1360. Temporary change of parameter <i>factor</i>.
  1361. </li><br>
  1362. <li>actor<br>
  1363. Set the actor state for this room to <i>on</i> or <i>off</i>. This is only a temporary change that will be overwritten by PWM object.
  1364. </li><br>
  1365. <li>desired-temp<br>
  1366. If <i>desired-temp</i> is automatically calculated (attribute <i>autoCalcTemp</i> not set or 1) then the desired temperature is set for a defined time.<br>
  1367. Default for this period is 60 minutes, but it can be changed by attribute <i>autoCalcTemp</i>.<br>
  1368. If <i>desired-temp</i> is not automatically calculated (attribute <i>autoCalcTemp</i> is 0) then this will set the actual target temperature.<br>
  1369. </li><br>
  1370. <li>manualTempDuration<br>
  1371. Define the period how long <i>desired-temp</i> manually set will be valid. Default is 60 Minutes.<br>
  1372. </li><br>
  1373. <li>interval<br>
  1374. Temporary change <i>INTERVAL</i> which defines how often <i>desired-temp</i> is calculated in autoCalcMode. Default is 300 seconds (5:00 Minutes).
  1375. </li><br>
  1376. <li>frostProtect<br>
  1377. Sets attribute frostProtect to 1 (on) or 0 (off).
  1378. </li><br>
  1379. </ul>
  1380. <b>Get </b>
  1381. <ul>
  1382. <li>previousTemps<br>
  1383. Get conent of buffers defined by <i>ILookBackCnt</i> and <i>DLookBackCnt</i>.
  1384. </li><br>
  1385. </ul>
  1386. <b>Attributes</b>
  1387. <ul>
  1388. <li>disable<br>
  1389. PWMR objects with attribute disable set to <i>1</i> will be excluded in the calculation loop of the PWM object.
  1390. </li><br>
  1391. <li>frostProtect<br>
  1392. Switch on (1) of off (0) frostProtectMode. <i>desired-temp</i> will be set to <i>tempFrostProtect</i> in autoCalcMode.
  1393. </li><br>
  1394. <li>autoCalcTemp<br>
  1395. Switch on (1) of off (0) autoCalcMode. <i>desired-temp</i> will be set based on the below temperatures and rules in autoCalcMode.<br>
  1396. Default is on.
  1397. </li><br>
  1398. <li>tempDay<br>
  1399. Define day temperature. This will be referenced as "D" in the rules.
  1400. </li><br>
  1401. <li>tempNight<br>
  1402. Define night temperature. This will be referenced as "N" in the rules.
  1403. </li><br>
  1404. <li>tempCosy<br>
  1405. Define cosy temperature. This will be referenced as "C" in the rules.
  1406. </li><br>
  1407. <li>tempEnergy<br>
  1408. Define energy saving temperature. This will be referenced as "E" in the rules.
  1409. </li><br>
  1410. <li>tempFrostProtect<br>
  1411. Define temperature for frostProtectMode. See also <i>frostProtect</i>.
  1412. </li><br>
  1413. <li>tempRule1 ... tempRule5<br>
  1414. Rule to calculate the <i>desired-temp</i> in autoCalcMode.<br>
  1415. Format is: &lt;weekday&gt;[-&lt;weekday] &lt;time&gt;,&lt;temperatureSelector&gt;<br>
  1416. weekday is one of Mo,Di,Mi,Do,Fr,Sa,So<br>
  1417. time is in format hh:mm, e.g. 7:00 or 07:00<br>
  1418. temperatureSelector is one of D,N,C,E<br>
  1419. <br>
  1420. Predefined are:<br>
  1421. tempRule1: Mo-Fr 6:00,D 22:00,N<br>
  1422. tempRule2: Sa-So 8:00,D 22:00,N<br>
  1423. This results in tempDay 6:00-22:00 from Monday to Friday and tempNight outside this time window.<br>
  1424. </li><br>
  1425. <li>desiredTempFrom<br>
  1426. This can be used as an alternative instead of the calculation of desired-temp based on the tempRules - which will happen when autoCalcTemp is set to '1'.<br>
  1427. (Either by removing the attribute autoCalcTemp or explicitly setting it to '1'.).<br>
  1428. If set correctly the desired-temp will be read from a reading of another device.<br>
  1429. Format is &lt;device&gt;[:&lt;reading&gt;[:&lt;regexp&gt;]]<br>
  1430. <i>device</i> defines the reference to the other object.<br>
  1431. <i>reading</i> defines the reading that contains the value for desired-temp. Default is 'desired-temp'.<br>
  1432. <i>regexp</i> defines a regular expression to extract the value used for 'desired-temp'. Default is '(\d[\d\.]+)'.
  1433. If <i>regexp</i> does not match (e.g. reading is 'off') then tempFrostProtect is used.<br>
  1434. Internals c_desiredTempFrom reflects the actual setting and d_name, d_reading und d_regexpTemp the values used.<br>
  1435. If this attribute is used then state will change from "Calculating" to "From &lt;device&gt;".<br>
  1436. Calculation of desired-temp is (like when using tempRules) based on the interval specified for this device (default is 300 seconds).<br>
  1437. Special values "on" and "off" of Homematic devices are handled as c_tempC (set by attribute tempCosy) and c_tempFrostProtect (set by attribute tempFrostProtect).
  1438. </li><br>
  1439. <li>valueFormat<br>
  1440. Defines a map to format values within PWMR.<br>
  1441. The following reading can be formated using syntax of sprinf: temperature
  1442. <br>
  1443. Example: { "temperature" => "%0.2f" }
  1444. </li><br>
  1445. </ul>
  1446. <br>
  1447. </ul>
  1448. =end html
  1449. =cut