94_PWM.pm 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. #
  2. #
  3. # 94_PWM.pm
  4. # written by Andreas Goebel 2012-07-25
  5. # e-mail: ag at goebel-it dot de
  6. #
  7. # 21.09.15 GA update, use Log3
  8. # 07.10.15 GA initial version published
  9. # 13.10.15 GA add event-on-change-reading
  10. # 13.10.15 GA add several readings
  11. # 15.10.15 GA add reading for avg pulses
  12. # 19.10.15 GA add overall heating switch
  13. # 22.10.15 GA add new definition for overall heating switch. Decision now based on threshold for pulseMax
  14. # 30.11.15 GA add new definition for overall heating switch. based on pulseMax or roomsOn
  15. # 30.11.15 GA add new followUpTime can now delay switching of OverallHeatingSwitch from "on" to "off"
  16. # 26.01.16 GA fix don't call AssignIoPort
  17. # 26.01.16 GA fix IODev from PWMR object is now a reference to PWM object
  18. # 29.06.16 GA add attribute valveProtectIdlePeriod
  19. # 16.08.16 GA add event-min-interval
  20. # 23.09.16 GA fix set default for maxPulse to 1 (from 0.85)
  21. # 28.09.16 GA add "get timers" to collect a maximum of all timers from the rooms attached
  22. # 11.10.16 GA add new delayTimeOn can now suspend switching of OverallHeadtingSwitch from "off" to "on"
  23. # 16.11.16 GA add new attribute overallHeatingSwitchRef for threshold based configuration
  24. # 17.11.16 GA add internals for configuration parameters: p_interval, p_cycletime, p_minOnOffTime,
  25. # p_maxPulse, p_roomsMinOnOffThreshold and p_overallHeatingSwitch
  26. # 01.08.17 GA add attribute disable to stop calculations of PWM
  27. # 01.08.17 GA fix OverallHeatingSwitch (without threshold) now independent from ValveProtection
  28. # 17.08.17 GA add attribute overallHeatingSwitchThresholdTemp define a threshold temperature to prevent switch to "on"
  29. # 30.11.17 GA add helper for last pulses of rooms
  30. # 30.11.17 GA fix clear roomsToStayOn and roomsToStayOnList if not used
  31. # 05.12.17 GA add extend helper for last pulses by $roomsWaitOffset{$wkey}
  32. # 13.12.17 GA fix consider $roomsWaitOffset{$wkey} in oldpulse set for each room
  33. # 31.01.18 GA add support for stateFormat
  34. # 05.02.18 GA fix typo overallHeatingSwitchThresholdTemup
  35. ##############################################
  36. # $Id: 94_PWM.pm 16090 2018-02-05 11:04:56Z jamesgo $
  37. # module for PWM (Pulse Width Modulation) calculation
  38. # this module uses PWMR (R like room) to
  39. # - get information (ReadRoom)
  40. # - set actors (SetRoom)
  41. #
  42. # standard heating devices support 0 to 100% heating they can be driven by the PID module
  43. # heating devices only supporing "on" of "off" can be driven by PWM
  44. # in PWM 50% is realised by defining a timeframe (cycletime)
  45. # and switch the defive "on" for 50% of this time
  46. # basis for calculation of this pulse is a factor multiplied with the difference
  47. # between desired-temp and act-temp
  48. #
  49. # default for cycletime is 15 minutes (900 sec)
  50. # since the devices act very slow
  51. # there is a parameter minonofftime to prevent "senseless" switches
  52. # PWM recalculates the needed pulse every 60 seconds and
  53. # then decides if the devices will be switched
  54. # "on->off", "off->on" or stays in the current state
  55. #
  56. package main;
  57. use strict;
  58. use warnings;
  59. sub PWM_Get($@);
  60. sub PWM_Set($@);
  61. sub PWM_Define($$);
  62. sub PWM_Calculate($);
  63. sub PWM_Undef($$);
  64. sub PWM_CalcRoom(@);
  65. my %roomsWaitOffset = ();
  66. ###################################
  67. sub
  68. PWM_Initialize($)
  69. {
  70. my ($hash) = @_;
  71. $hash->{GetFn} = "PWM_Get";
  72. $hash->{SetFn} = "PWM_Set";
  73. $hash->{DefFn} = "PWM_Define";
  74. $hash->{UndefFn} = "PWM_Undef";
  75. $hash->{AttrFn} = "PWM_Attr";
  76. $hash->{AttrList} = "disable:1,0 valveProtectIdlePeriod overallHeatingSwitchRef:pulseMax,pulseSum,pulseAvg,pulseAvg2,pulseAvg3,avgPulseRoomsOn".
  77. " overallHeatingSwitchThresholdTemp ".$readingFnAttributes;
  78. #$hash->{GetList} = "status timers";
  79. }
  80. ###################################
  81. sub
  82. PWM_Calculate($)
  83. {
  84. my ($hash) = @_;
  85. my $name = $hash->{NAME};
  86. my %RoomsToSwitchOn = ();
  87. my %RoomsToSwitchOff = ();
  88. my %RoomsToStayOn = ();
  89. my %RoomsToStayOff = ();
  90. my %RoomsValveProtect = ();
  91. my %RoomsPulses = ();
  92. my $roomsActive = 0;
  93. my $newpulseMax = 0;
  94. my $newpulseSum = 0;
  95. my $newpulseAvg = 0;
  96. my $newpulseAvg2 = 0;
  97. my $newpulseAvg3 = 0;
  98. my $wkey = "";
  99. if($hash->{INTERVAL} > 0) {
  100. InternalTimer(gettimeofday() + $hash->{INTERVAL}, "PWM_Calculate", $hash, 0);
  101. }
  102. if (defined($attr{$name}{disable}) and $attr{$name}{disable} == 1) {
  103. Log3 ($hash, 3, "PWM_Calculate $name");
  104. #$hash->{STATE} = "disabled";
  105. readingsSingleUpdate ($hash, "state", "disabled", 1);
  106. readingsSingleUpdate ($hash, "lastrun", "disabled", 0);
  107. return;
  108. }
  109. Log3 ($hash, 3, "PWM_Calculate $name");
  110. $hash->{helper}{pulses} = ();
  111. readingsBeginUpdate ($hash);
  112. readingsBulkUpdate ($hash, "lastrun", "calculating");
  113. #$hash->{STATE} = "lastrun: ".$hash->{READINGS}{lastrun}{TIME};
  114. readingsBulkUpdate ($hash, "state", "lastrun: ".$hash->{READINGS}{lastrun}{TIME});
  115. # loop over all devices
  116. # fetch all PWMR devices
  117. # which are not disabled
  118. # and are linked to me (via IODev)
  119. foreach my $d (sort keys %defs) {
  120. if ( (defined ($defs{$d}{TYPE})) && $defs{$d}{TYPE} eq "PWMR" ) { # all PWMR objects
  121. if (!defined ($attr{$d}{disable}) or $attr{$d}{disable} == 0) { # not disabled
  122. if ($hash->{NAME} eq $defs{$d}{IODev}->{NAME}) { # referencing to this fb
  123. Log3 ($hash, 4, "PWM_Calculate calc $name, room $d");
  124. ########################
  125. # calculate room
  126. # $newstate is "" if state is unchanged
  127. # $newstate is "on" or "off" if state changes
  128. # $newstate may be "on_vp" or "off_vp" if valve protection is active
  129. my ($newstate, $newpulse, $cycletime, $oldstate) = PWM_CalcRoom($hash, $defs{$d});
  130. my $onoff = $newpulse * $cycletime;
  131. if ($newstate =~ "off.*") {
  132. $onoff = (1 - $newpulse) * $cycletime
  133. }
  134. if ($newstate eq "on_vp") {
  135. $RoomsValveProtect{$d} = "on";
  136. } elsif ($newstate eq "off_vp") {
  137. $RoomsValveProtect{$d} = "off";
  138. }
  139. $wkey = $name."_".$d;
  140. if (defined ($roomsWaitOffset{$wkey})) {
  141. $hash->{helper}{pulses}{$d} = $newpulse." / ".$roomsWaitOffset{$wkey};
  142. $newpulse += $roomsWaitOffset{$wkey};
  143. } else {
  144. $roomsWaitOffset{$wkey} = 0;
  145. $hash->{helper}{pulses}{$d} = $newpulse." / ".$roomsWaitOffset{$wkey};
  146. }
  147. $defs{$d}->{READINGS}{oldpulse}{TIME} = TimeNow();
  148. $defs{$d}->{READINGS}{oldpulse}{VAL} = $newpulse;
  149. $roomsActive++;
  150. $RoomsPulses{$d} = $newpulse;
  151. $newpulseSum += $newpulse;
  152. $newpulseMax = max($newpulseMax, $newpulse);
  153. # $newstate ne "" -> state changed "on" -> "off" or "off" -> "on"
  154. if ((int($hash->{MINONOFFTIME}) > 0) &&
  155. (($newstate eq "on") or ($newstate eq "off")) &&
  156. ($onoff < int($hash->{MINONOFFTIME}))
  157. ) {
  158. #######################
  159. # actor devices take 3 minutes for an open/close cycle
  160. # this is handled by MINONOFFTIME
  161. Log3 ($hash, 3, "PWM_Calculate $d: F0 stay unchanged $oldstate: ".
  162. "($onoff < $hash->{MINONOFFTIME} sec)");
  163. if ($oldstate eq "off") {
  164. $RoomsToStayOff{$d} = $newpulse;
  165. } else {
  166. $RoomsToStayOn{$d} = $newpulse;
  167. }
  168. } else {
  169. # state changed and it is worth to move the device
  170. if ($newstate eq "on") {
  171. $RoomsToSwitchOn{$d} = $newpulse;
  172. } elsif ($newstate eq "off") {
  173. $RoomsToSwitchOff{$d} = $newpulse;
  174. } elsif ($newstate eq "") {
  175. if ($oldstate eq "on") {
  176. $RoomsToStayOn{$d} = $newpulse;
  177. } else {
  178. $RoomsToStayOff{$d} = $newpulse;
  179. }
  180. }
  181. }
  182. }
  183. }
  184. }
  185. }
  186. # synchronize the heating on the "off" edge of the pulse
  187. # try to minimize the situation where all rooms are "on" at the same time
  188. #
  189. # algorithm:
  190. # -> if more than 2 rooms are switched off at the same time,
  191. # -> simply keep some on (but this will last only for one calculation cycle)
  192. #
  193. # assumption: 100% "on" time is not allowed (max newpulse = 85%)
  194. # -> in the morning all rooms will be switched on at the same time
  195. # -> and then off at the same time
  196. # normally we switch off only one room at the same time
  197. # normally we switch on only one room at the same time
  198. my $switchOn = $hash->{MaxSwitchOnPerCycle}; # default 1
  199. my $switchOff = $hash->{MaxSwitchOffPerCycle}; # default 1
  200. # rooms may stay on due to logic below ...
  201. #
  202. # switch off only (one) the room with lowest need for heating
  203. # sort rooms with ascending "newpulse"
  204. foreach my $room (sort { $RoomsToSwitchOff{$a} <=> $RoomsToSwitchOff{$b} } keys %RoomsToSwitchOff) {
  205. # only the first room in the list will be switched off
  206. # all others will stay on
  207. # first room has the lowest need for heating ... it will be switched off
  208. $switchOff--;
  209. if ($switchOff >= 0) {
  210. Log3 ($hash, 3, "PWM_Calculate $room: F99 switch off ".
  211. "(pulse=$RoomsToSwitchOff{$room})");
  212. next;
  213. }
  214. Log3 ($hash, 3, "PWM_Calculate $room: F99 keep room on ".
  215. "(pulse=$RoomsToSwitchOff{$room})");
  216. $RoomsToStayOn{$room} = 1;
  217. if (defined($RoomsToSwitchOff{$room})) {
  218. delete ($RoomsToSwitchOff{$room});
  219. }
  220. }
  221. # try to minimize the situation where all rooms are "on" at the same time
  222. # switch "on" only one room at the same time
  223. # sort rooms with decending "newpulse"
  224. foreach my $room (sort { $RoomsToSwitchOn{$b} <=> $RoomsToSwitchOn{$a} } keys %RoomsToSwitchOn) {
  225. # only the first room in the list will be switched on
  226. # all others will stay off
  227. # first room has the highest need for heating ... it will be switched on
  228. $switchOn--;
  229. if ($switchOn >= 0) {
  230. Log3 ($hash, 3, "PWM_Calculate $room: F98 switch on ".
  231. "(pulse=$RoomsToSwitchOn{$room})");
  232. next;
  233. }
  234. Log3 ($hash, 3, "PWM_Calculate $room: F98 keep room off ".
  235. "(pulse=$RoomsToSwitchOn{$room})");
  236. my $wkey = $name."_".$room;
  237. $roomsWaitOffset{$wkey} += 0.0001;
  238. $RoomsToStayOff{$room} = 1;
  239. if (defined($RoomsToSwitchOn{$room})) {
  240. delete ($RoomsToSwitchOn{$room});
  241. }
  242. }
  243. # in addition to the above max. of 85% of the active rooms may be on at the same time
  244. # 11 * 0.8 = 8.8 ... 8 is ok ... 9, 10, 11 is not (laraEG!)
  245. my $roomsOn = (scalar keys %RoomsToStayOn) - (scalar keys %RoomsToSwitchOff);
  246. # treat less than 8 active rooms as 8 (more can get active)
  247. # 16.01.2015
  248. #my $maxRoomsOn = $roomsActive * 0.7;
  249. # 23.09.2015
  250. #my $maxRoomsOn = $roomsActive * 0.6; # 11 rooms -> max 6 active
  251. #$maxRoomsOn = (8 * 0.7) if ($roomsActive < 8);
  252. my $maxRoomsOn = $roomsActive - $hash->{NoRoomsToStayOff};
  253. #
  254. # looks complicated but this will work if more than one room would be switched on
  255. #
  256. # prevent rooms to be switched on if maxRoomsOn is reached
  257. #
  258. while (
  259. (($roomsOn + (scalar keys %RoomsToSwitchOn)) > $maxRoomsOn) &&
  260. ((scalar keys %RoomsToSwitchOn) > 0)
  261. ) {
  262. # sort rooms with ascending "newpulse"
  263. foreach my $room (sort { $RoomsToSwitchOn{$a} <=> $RoomsToSwitchOn{$b} } keys %RoomsToSwitchOn) {
  264. Log3 ($hash, 3, "PWM_Calculate $room: F97 keep room off ".
  265. "(pulse=$RoomsToSwitchOn{$room}) (max=$maxRoomsOn)");
  266. my $wkey = $name."_".$room;
  267. $roomsWaitOffset{$wkey} += 0.001;
  268. $RoomsToStayOff{$room} = 1;
  269. if (defined($RoomsToSwitchOn{$room})) {
  270. delete ($RoomsToSwitchOn{$room});
  271. }
  272. last; # continue in while loop
  273. }
  274. }
  275. # in addition to the above try to prevent that too many rooms are off
  276. # use $roomsActive and $newpulseSum to differentiate if heating is required
  277. # 11 * 0.27 = 2.97 ... 3 rooms is ok ... 0,1 or 2 is not
  278. # 23.09.2015
  279. #my $minRoomsOn = $roomsActive * 0.29;
  280. # if overall required heating is below 0.42 ... possibly drive Vaillant into "Sperrzeit"
  281. # 15.01.2015: adjust this from 0.42 to 0.25 (=25% Pulse needed)
  282. # 23.09.2015
  283. #if ($roomsActive == 0 or $newpulseSum/$roomsActive < 0.42) {
  284. # $minRoomsOn = 0;
  285. #}
  286. my $minRoomsOn = $hash->{NoRoomsToStayOn};
  287. my $minRoomsOnList = "";
  288. if ($minRoomsOn > 0) {
  289. my $roomsCounted = 0;
  290. my $pulseSum = 0;
  291. foreach my $room (sort { $RoomsPulses{$b} <=> $RoomsPulses{$a} } keys %RoomsPulses) {
  292. last if ($roomsCounted == $minRoomsOn);
  293. Log3 ($hash, 3, "PWM_Calculate: loop $roomsCounted $room $RoomsPulses{$room}");
  294. $minRoomsOnList .= "$room,";
  295. $pulseSum += $RoomsPulses{$room};
  296. $roomsCounted++;
  297. }
  298. $minRoomsOnList =~ s/,$//;
  299. if ($roomsActive == 0 or $hash->{NoRoomsToStayOnThreshold} == 0 or $pulseSum/$roomsCounted < $hash->{NoRoomsToStayOnThreshold}) {
  300. $minRoomsOn = 0;
  301. $minRoomsOnList = "";
  302. }
  303. #Log3 ($hash, 3, "PWM_Calculate: newpulseSum $newpulseSum avg ".$newpulseSum/$roomsActive." minRoomsOn(".$minRoomsOn.")") if ($roomsActive > 0);
  304. Log3 ($hash, 3, "PWM_Calculate: pulseSum $pulseSum avg ".$pulseSum/$roomsCounted." minRoomsOn(".$minRoomsOn.")") if ($roomsActive > 0);
  305. }
  306. #
  307. # looks complicated but this will work if more than one room would stay on
  308. #
  309. while (
  310. (((scalar keys %RoomsToStayOn) + (scalar keys %RoomsToSwitchOn)) < $minRoomsOn) &&
  311. ((scalar keys %RoomsToSwitchOff) > 0)
  312. ) {
  313. # sort rooms with decending "newpulse"
  314. foreach my $room (sort { $RoomsToSwitchOff{$b} <=> $RoomsToSwitchOff{$a} } keys %RoomsToSwitchOff) {
  315. my $ron = 1 + (scalar keys %RoomsToStayOn) + (scalar keys %RoomsToSwitchOn);
  316. Log3 ($hash, 3, "PWM_Calculate $room: F96 keep room on ".
  317. "(pulse=$RoomsToSwitchOff{$room}) (min=$minRoomsOn) (roomsOn=$ron)");
  318. my $wkey = $name."_".$room;
  319. $roomsWaitOffset{$wkey} -= 0.001;
  320. $RoomsToStayOn{$room} = 1;
  321. if (defined($RoomsToSwitchOff{$room})) {
  322. delete ($RoomsToSwitchOff{$room});
  323. }
  324. last; # continue in while loop
  325. }
  326. }
  327. #
  328. # now process the calculated actions
  329. #
  330. my $cntRoomsOn = 0;
  331. my $cntRoomsOnVP = 0;
  332. my $cntRoomsOff = 0;
  333. my $pulseRoomsOn = 0;
  334. my $pulseRoomsOff = 0;
  335. foreach my $roomStay (sort keys %RoomsToStayOff) {
  336. PWMR_SetRoom ($defs{$roomStay}, "");
  337. $cntRoomsOff++;
  338. $pulseRoomsOff += $RoomsPulses{$roomStay};
  339. }
  340. foreach my $roomStay (sort keys %RoomsToStayOn) {
  341. PWMR_SetRoom ($defs{$roomStay}, "");
  342. $cntRoomsOn++;
  343. $pulseRoomsOn += $RoomsPulses{$roomStay};
  344. }
  345. foreach my $roomOff (sort keys %RoomsToSwitchOff) {
  346. PWMR_SetRoom ($defs{$roomOff}, "off");
  347. $cntRoomsOff++;
  348. $pulseRoomsOff += $RoomsPulses{$roomOff};
  349. }
  350. foreach my $roomOn (sort keys %RoomsToSwitchOn) {
  351. my $wkey = $name."-".$roomOn;
  352. $roomsWaitOffset{$wkey} = 0;
  353. PWMR_SetRoom ($defs{$roomOn}, "on");
  354. $cntRoomsOn++;
  355. $pulseRoomsOn += $RoomsPulses{$roomOn};
  356. }
  357. foreach my $roomVP (sort keys %RoomsValveProtect) {
  358. my $wkey = $name."-".$roomVP;
  359. $roomsWaitOffset{$wkey} = 0;
  360. if ( $RoomsValveProtect{$roomVP} eq "on") {
  361. PWMR_SetRoom ($defs{$roomVP}, "on");
  362. $cntRoomsOn++;
  363. $cntRoomsOnVP++;
  364. $pulseRoomsOn += $RoomsPulses{$roomVP};
  365. } else {
  366. PWMR_SetRoom ($defs{$roomVP}, "off");
  367. $cntRoomsOff++;
  368. $pulseRoomsOff += $RoomsPulses{$roomVP};
  369. }
  370. }
  371. my $cntAvg = 0;
  372. # sort rooms with decending "newpulse"
  373. foreach my $room (sort { $RoomsPulses{$b} <=> $RoomsPulses{$a} } keys %RoomsPulses) {
  374. $newpulseAvg += $RoomsPulses{$room};
  375. $newpulseAvg2 += $RoomsPulses{$room} if ($cntAvg < 2);
  376. $newpulseAvg3 += $RoomsPulses{$room} if ($cntAvg < 3);
  377. $cntAvg++;
  378. }
  379. $newpulseAvg = sprintf ("%.02f", $newpulseAvg / $cntAvg) if ($cntAvg > 0);
  380. $newpulseAvg2 = sprintf ("%.02f", $newpulseAvg2 / minNum (2, $cntAvg)) if ($cntAvg > 0);
  381. $newpulseAvg3 = sprintf ("%.02f", $newpulseAvg3 / minNum (3, $cntAvg)) if ($cntAvg > 0);
  382. readingsBulkUpdate ($hash, "roomsActive", $roomsActive);
  383. readingsBulkUpdate ($hash, "roomsOn", $cntRoomsOn);
  384. readingsBulkUpdate ($hash, "roomsOff", $cntRoomsOff);
  385. readingsBulkUpdate ($hash, "avgPulseRoomsOn", ($cntRoomsOn > 0 ? sprintf ("%.2f", $pulseRoomsOn / $cntRoomsOn) : 0));
  386. readingsBulkUpdate ($hash, "avgPulseRoomsOff", ($cntRoomsOff > 0 ? sprintf ("%.2f", $pulseRoomsOff /$cntRoomsOff) : 0));
  387. readingsBulkUpdate ($hash, "pulseMax", $newpulseMax);
  388. readingsBulkUpdate ($hash, "pulseSum", $newpulseSum);
  389. readingsBulkUpdate ($hash, "pulseAvg", $newpulseAvg);
  390. readingsBulkUpdate ($hash, "pulseAvg2", $newpulseAvg2);
  391. readingsBulkUpdate ($hash, "pulseAvg3", $newpulseAvg3);
  392. if ( $hash->{NoRoomsToStayOn} > 0) {
  393. readingsBulkUpdate ($hash, "roomsToStayOn", $minRoomsOn);
  394. readingsBulkUpdate ($hash, "roomsToStayOnList", $minRoomsOnList);
  395. } else {
  396. readingsBulkUpdate ($hash, "roomsToStayOn", 0);
  397. readingsBulkUpdate ($hash, "roomsToStayOnList", "");
  398. }
  399. if ( defined ($hash->{OverallHeatingSwitch}) ) {
  400. if ( $hash->{OverallHeatingSwitch} ne "") {
  401. my $newstateOHS = "on";
  402. if ( $hash->{OverallHeatingSwitch_threshold} > 0) {
  403. # threshold based
  404. my $refValue = $newpulseMax;
  405. if (defined($attr{$name}{overallHeatingSwitchRef})) {
  406. my $ref = $attr{$name}{overallHeatingSwitchRef};
  407. $refValue = $newpulseMax if ($ref eq "pulseMax");
  408. $refValue = $newpulseSum if ($ref eq "pulseSum");
  409. $refValue = $newpulseAvg if ($ref eq "pulseAvg");
  410. $refValue = $newpulseAvg2 if ($ref eq "pulseAvg2");
  411. $refValue = $newpulseAvg3 if ($ref eq "pulseAvg3");
  412. }
  413. $newstateOHS = ($refValue > $hash->{OverallHeatingSwitch_threshold}) ? "on" : "off";
  414. } else {
  415. # room based
  416. $newstateOHS = (($cntRoomsOn - $cntRoomsOnVP) > 0) ? "on" : "off";
  417. }
  418. # OverallHeatingSwitchThresholdTemp may prevent switch ot on and sets OverallHeatingSwitch to e-off
  419. my $newstateOHS_eoff = 0;
  420. if ($newstateOHS eq "on" and defined ($hash->{OverallHeatingSwitchTT_tsensor})) {
  421. my $sensor = $hash->{OverallHeatingSwitchTT_tsensor};
  422. my $reading = $hash->{OverallHeatingSwitchTT_reading};
  423. if (defined ($defs{$sensor}) and defined ($defs{$sensor}->{READINGS}{$reading})) {
  424. my $t_regexp = $hash->{OverallHeatingSwitchTT_t_regexp};
  425. my $maxTemp = $hash->{OverallHeatingSwitchTT_maxTemp};
  426. my $temp = $defs{$sensor}->{READINGS}{$reading}{VAL};
  427. $temp =~ /$t_regexp/;
  428. if (defined ($1))
  429. {
  430. $temp = $1;
  431. if ($temp >= $maxTemp)
  432. {
  433. $newstateOHS_eoff = 1;
  434. $newstateOHS = "off";
  435. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitch forced to off since ThresholdTemp reached maxTemp ($temp >= $maxTemp)");
  436. readingsBulkUpdate ($hash, "OverallHeatingSwitchTT_Off", 1);
  437. }
  438. else
  439. {
  440. if ($hash->{READINGS}{OverallHeatingSwitchTT_Off}{VAL} == 1) {
  441. readingsBulkUpdate ($hash, "OverallHeatingSwitchTT_Off", 0);
  442. }
  443. }
  444. }
  445. else
  446. {
  447. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitchThresholdTemp t_regexp does not match temperature");
  448. }
  449. }
  450. else
  451. {
  452. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitchThresholdTemp refers to invalid device or reading");
  453. }
  454. }
  455. my $actor = $hash->{OverallHeatingSwitch};
  456. my $actstateOHS = ($defs{$actor}{STATE} =~ $hash->{OverallHeatingSwitch_regexp_on}) ? "on" : "off";
  457. if ($hash->{OverallHeatingSwitch_followUpTime} > 0) {
  458. if ($newstateOHS_eoff == 1)
  459. {
  460. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", "");
  461. }
  462. else
  463. {
  464. if ($actstateOHS eq "on" and $newstateOHS eq "off") {
  465. if ($hash->{READINGS}{OverallHeatingSwitchWaitUntilOff}{VAL} eq "") {
  466. $newstateOHS = "on";
  467. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitch wait for followUpTime before switching off (init timestamp)");
  468. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", FmtDateTime(time() + $hash->{OverallHeatingSwitch_followUpTime}));
  469. } elsif ($hash->{READINGS}{OverallHeatingSwitchWaitUntilOff}{VAL} ge TimeNow()) {
  470. $newstateOHS = "on";
  471. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitch wait for followUpTime before switching off");
  472. } else {
  473. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", "");
  474. }
  475. } else {
  476. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", "");
  477. }
  478. }
  479. }
  480. if ($hash->{OverallHeatingSwitch_delayTimeOn} > 0) {
  481. if ($actstateOHS eq "off" and $newstateOHS eq "on") {
  482. if ($hash->{READINGS}{OverallHeatingSwitchWaitBeforeOn}{VAL} eq "") {
  483. $newstateOHS = "off";
  484. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitch wait for delayTimeOn before switching on (init timestamp)");
  485. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitBeforeOn", FmtDateTime(time() + $hash->{OverallHeatingSwitch_delayTimeOn}));
  486. } elsif ($hash->{READINGS}{OverallHeatingSwitchWaitBeforeOn}{VAL} ge TimeNow()) {
  487. $newstateOHS = "off";
  488. Log3 ($name, 2, "PWM_Calculate: $name: OverallHeatingSwitch wait for delayTimeOn before switching on");
  489. } else {
  490. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitBeforeOn", "");
  491. }
  492. } else {
  493. readingsBulkUpdate ($hash, "OverallHeatingSwitchWaitBeforeOn", "");
  494. }
  495. }
  496. if ($newstateOHS ne $actstateOHS or $hash->{READINGS}{OverallHeatingSwitch}{VAL} ne $actstateOHS) {
  497. my $ret = fhem sprintf ("set %s %s", $hash->{OverallHeatingSwitch}, $newstateOHS);
  498. if (!defined($ret)) { # sucessfull
  499. Log3 ($name, 4, "PWMR_SetRoom: $name: set $actor $newstateOHS");
  500. readingsBulkUpdate ($hash, "OverallHeatingSwitch", $newstateOHS);
  501. # push @{$room->{CHANGED}}, "actor $newstateOHS";
  502. # DoTrigger($name, undef);
  503. } else {
  504. Log3 ($name, 4, "PWMR_SetRoom $name: set $actor $newstateOHS failed ($ret)");
  505. }
  506. }
  507. }
  508. }
  509. readingsEndUpdate($hash, 1);
  510. # if(!$hash->{LOCAL}) {
  511. # DoTrigger($name, undef) if($init_done);
  512. # }
  513. }
  514. ###################################
  515. sub
  516. PWM_CalcRoom(@)
  517. {
  518. my ($hash, $room) = @_;
  519. my $name = $hash->{NAME};
  520. Log3 ($hash, 4, "PWM_CalcRoom: $name ($room->{NAME})");
  521. my $cycletime = $hash->{CYCLETIME};
  522. my ($temperaturV, $actorV, $factor, $oldpulse, $newpulse, $prevswitchtime, $windowV) =
  523. PWMR_ReadRoom($room, $cycletime, $hash->{MaxPulse});
  524. my $nextswitchtime;
  525. if ($actorV eq "on") {
  526. $nextswitchtime = int($oldpulse * $cycletime) + $prevswitchtime;
  527. } else {
  528. $nextswitchtime = int((1-$oldpulse) * $cycletime) + $prevswitchtime;
  529. }
  530. #Log3 ($hash, 4, "PWM_CalcRoom $room->{NAME}: $cycletime ($prevswitchtime/$nextswitchtime)=".($nextswitchtime-$prevswitchtime));
  531. if ($actorV eq "on") # current state is "on"
  532. {
  533. # ----------------
  534. # check if valve protection is active, keep this state for 5 minutes
  535. if (defined ($room->{helper}{valveProtectLastSwitch})) {
  536. if ( $room->{helper}{valveProtectLastSwitch} + 300 > time()) {
  537. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F13 valveProtect continue");
  538. return ("", $newpulse, $cycletime, $actorV);
  539. } else {
  540. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F14 valveProtect off");
  541. delete ($room->{helper}{valveProtectLastSwitch});
  542. return ("off_vp", $newpulse, $cycletime, $actorV);
  543. }
  544. }
  545. # ----------------
  546. # decide if to change to "off"
  547. if ($newpulse == 1) {
  548. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F10 stay on");
  549. return ("", $newpulse, $cycletime, $actorV);
  550. }
  551. if ($newpulse < $oldpulse) { # on: was 80% now it is 30%
  552. if ( time() >= $nextswitchtime ) # F3
  553. {
  554. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F3 new off");
  555. return ("off", $newpulse, $cycletime, $actorV);
  556. # state changed and it is worth to move the device
  557. }
  558. else #( time() < $nextswitchtime ) # F1
  559. {
  560. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F1 stay on");
  561. return ("", $newpulse, $cycletime, $actorV);
  562. }
  563. } else { #($newpulse >= $oldpulse) # unchanged, or was 30% now 40%
  564. # maybe we switch off
  565. # - because several cycles were not calculated
  566. # - or on time is simply over
  567. # - newpulse 0 is also handled here
  568. if ( time() >= $nextswitchtime) { # F4
  569. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F4 new off");
  570. return ("off", $newpulse, $cycletime, $actorV);
  571. } else {
  572. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F9 stay on");
  573. return ("", $newpulse, $cycletime, $actorV);
  574. }
  575. }
  576. }
  577. elsif ($actorV eq "off") # current state is "off"
  578. {
  579. # ----------------
  580. # check if valve protection is activated (attribute valveProtectIdlePeriod is set)
  581. if (defined ($attr{$name}{"valveProtectIdlePeriod"})) {
  582. # period is defined in days (*86400)
  583. if ($room->{READINGS}{lastswitch}{VAL} + ($attr{$name}{"valveProtectIdlePeriod"} * 86400) < time()) {
  584. $room->{helper}{valveProtectLastSwitch} = time();
  585. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F12 valve protect");
  586. return ("on_vp", $newpulse, $cycletime, $actorV);
  587. }
  588. }
  589. # ----------------
  590. # decide if to change to "on"
  591. if ($oldpulse == 0 && $newpulse > 0) { # was 0% now heating is required
  592. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F7 new on");
  593. return ("on", $newpulse, $cycletime, $actorV);
  594. }
  595. if ($newpulse == 0) {
  596. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F11 stay off (0)");
  597. return ("", $newpulse, $cycletime, $actorV);
  598. }
  599. if ($newpulse > $oldpulse) { # was 30% now it is 80%
  600. # F5
  601. if ( time() < $nextswitchtime )
  602. {
  603. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F5 stay off");
  604. return ("", $newpulse, $cycletime, $actorV);
  605. }
  606. else # time >= $nextswitchtime
  607. {
  608. # F6
  609. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F6 new on");
  610. return ("on", $newpulse, $cycletime, $actorV);
  611. }
  612. } else { # unchanged, was 80% now 30%
  613. # F2
  614. if ( time() >= $nextswitchtime ) {
  615. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F2 new on");
  616. return ("on", $newpulse, $cycletime, $actorV);
  617. } else {
  618. Log3 ($hash, 3, "PWM_CalcRoom $room->{NAME}: F8 stay off");
  619. return ("", $newpulse, $cycletime, $actorV);
  620. }
  621. }
  622. }
  623. else # $actorV not "on" of "off"
  624. {
  625. Log3 ($hash, 3, "PWM_CalcRoom -> $name -> $room->{NAME}: invalid actor state ($actorV) try to switch off");
  626. return ("off", 0, $cycletime, $actorV);
  627. }
  628. return ("", $newpulse, $cycletime, $actorV);
  629. }
  630. ###################################
  631. sub
  632. PWM_Get($@)
  633. {
  634. my ($hash, @a) = @_;
  635. my $name = $hash->{NAME};
  636. return "argument is missing" if(int(@a) != 2);
  637. my $msg;
  638. if ($a[1] eq "status") {
  639. return $hash->{STATE};
  640. } elsif ($a[1] eq "timers") {
  641. Log3 ($hash, 1, "in get timers");
  642. my $cnt = 0;
  643. my %tmpTimersFrom = ();
  644. my %tmpTimersTo = ();
  645. foreach my $d (sort keys %defs) {
  646. if ( (defined ($defs{$d}{TYPE})) && $defs{$d}{TYPE} eq "PWMR" ) { # all PWMR objects
  647. if (!defined ($attr{$d}{disable}) or $attr{$d}{disable} == 0) { # not disabled
  648. if ($name eq $defs{$d}{IODev}->{NAME}) { # referencing to this fb
  649. my $room = $defs{$d};
  650. Log3 ($hash, 1, "PWM_Get $name collect $room->{NAME}");
  651. foreach my $reading ("timer1_Mo", "timer2_Di", "timer3_Mi", "timer4_Do", "timer5_Fr", "timer6_Sa", "timer7_So") {
  652. if (defined ($room->{READINGS}{$reading}) and $room->{READINGS}{$reading} ne "") {
  653. $cnt++;
  654. Log3 ($hash, 1, "PWM_Get $name collect $room->{NAME} $reading");
  655. my (@timers) = split / /, $room->{READINGS}{$reading}{VAL};
  656. my ($mintime, $minTempId, $minTemp ) = split /,/, $timers[0];
  657. my ($maxtime, $maxTempId, $maxTemp ) = split /,/, $timers[$#timers];
  658. my ($minfrom, $minto) = split /-/, $mintime;
  659. my ($maxfrom, $maxto) = split /-/, $maxtime;
  660. $tmpTimersFrom{$reading} = $minfrom unless defined($tmpTimersFrom{$reading});
  661. $tmpTimersTo{$reading} = $maxto unless defined($tmpTimersTo{$reading});
  662. $tmpTimersFrom{$reading} = $minfrom if ($tmpTimersFrom{$reading} > $minfrom);
  663. $tmpTimersTo{$reading} = $maxto if ($tmpTimersTo{$reading} < $maxto);
  664. }
  665. }
  666. }
  667. }
  668. }
  669. }
  670. if ($cnt == 0) {
  671. foreach my $reading ("timer1_Mo", "timer2_Di", "timer3_Mi", "timer4_Do", "timer5_Fr", "timer6_Sa", "timer7_So") {
  672. delete ($hash->{READINGS}{$reading});
  673. }
  674. } else {
  675. readingsBeginUpdate ($hash);
  676. foreach my $reading ("timer1_Mo", "timer2_Di", "timer3_Mi", "timer4_Do", "timer5_Fr", "timer6_Sa", "timer7_So") {
  677. readingsBulkUpdate ($hash, "$reading", $tmpTimersFrom{$reading}."-".$tmpTimersTo{$reading});
  678. }
  679. readingsEndUpdate($hash, 1);
  680. }
  681. #return "$reading from $minfrom to $maxto";
  682. return "";
  683. } else {
  684. return "Unknown argument $a[1], choose one of status timers";
  685. }
  686. }
  687. #############################
  688. sub
  689. PWM_Set($@)
  690. {
  691. my ($hash, @a) = @_;
  692. my $u = "Unknown argument $a[1], choose one of recalc interval cycletime";
  693. if ( $a[1] =~ /^interval$|^cycletime$/ ) {
  694. return $u if(int(@a) != 3);
  695. my $hw = uc($a[1]);
  696. $hash->{$hw}= $a[2];
  697. } elsif ( $a[1] =~ /^recalc$/ ) {
  698. #$hash->{LOCAL} = 1;
  699. RemoveInternalTimer($hash);
  700. my $v = PWM_Calculate($hash);
  701. #delete $hash->{LOCAL};
  702. } else {
  703. return $u;
  704. }
  705. return undef;
  706. }
  707. #############################
  708. sub
  709. PWM_Define($$)
  710. {
  711. my ($hash, $def) = @_;
  712. my @a = split("[ \t][ \t]*", $def);
  713. my $name = $hash->{NAME};
  714. return "syntax: define <name> PWM [<interval>] [<cycletime>] [<minonofftime>] [<maxPulse>] [<maxSwitchOnPerCycle>,<maxSwitchOffPerCycle>] [<roomStayOn>,<roomStayOff>,<stayOnThreshold>]".
  715. " [<overallHeatingSwitch>[,<pulseThreshold>[,<followUpTime>[,<h_regexp_on>[,<delayTimeOn>]]]]"
  716. if(int(@a) < 2 || int(@a) > 9);
  717. my $interval = ((int(@a) > 2) ? $a[2] : 60);
  718. my $cycletime = ((int(@a) > 3) ? $a[3] : 900);
  719. my $minonofftime = ((int(@a) > 4) ? $a[4] : 120);
  720. my $maxPulse = ((int(@a) > 5) ? minNum ($a[5], 1.00) : 1.00);
  721. $hash->{INTERVAL} = $interval;
  722. $hash->{CYCLETIME} = $cycletime;
  723. $hash->{MINONOFFTIME} = $minonofftime;
  724. $hash->{MaxPulse} = $maxPulse;
  725. $hash->{STATE} = "defined";
  726. $hash->{p_interval} = $interval;
  727. $hash->{p_cycletime} = $cycletime;
  728. $hash->{p_minOnOfftime} = $minonofftime;
  729. $hash->{p_maxPulse} = $maxPulse;
  730. ##########
  731. # [<maxSwitchOnPerCycle>,<maxSwitchOffPerCycle>]
  732. if (int(@a) > 6) {
  733. my ($maxOn, $maxOff) = split (",", $a[6]);
  734. $maxOff = $maxOn unless (defined($maxOff));
  735. $hash->{MaxSwitchOnPerCycle} = $maxOn;
  736. $hash->{MaxSwitchOffPerCycle} = $maxOff;
  737. } else {
  738. if ($maxPulse == 1) {
  739. $hash->{MaxSwitchOnPerCycle} = 99;
  740. $hash->{MaxSwitchOffPerCycle} = 99;
  741. } else {
  742. $hash->{MaxSwitchOnPerCycle} = 1;
  743. $hash->{MaxSwitchOffPerCycle} = 1;
  744. }
  745. }
  746. ##########
  747. # [<roomStayOn>,<roomStayOff>,<stayOnThreshold>]
  748. if (int(@a) > 7) {
  749. my ($stayOn, $stayOff, $onThreshold) = split (",", $a[7]);
  750. $stayOff = 1 unless (defined($stayOff)); # one room stays off
  751. $onThreshold = 0.3 unless (defined($onThreshold)); # $stayOn is used only if average pluse is >= 0.3
  752. $hash->{NoRoomsToStayOn} = $stayOn; # eg. 4 rooms stay switched on (unless average pulse is less then threshold)
  753. $hash->{NoRoomsToStayOff} = $stayOff; # 1 room stays off to limit energy used (maxPulse should be < 1 if this is used)
  754. $hash->{NoRoomsToStayOnThreshold} = $onThreshold; # $stayOn is used only if average pluse is >= threshold
  755. $hash->{p_roomsMinOnOffThreshold} = $a[7];
  756. } else {
  757. $hash->{NoRoomsToStayOn} = 0; # switch off all rooms is allowd
  758. $hash->{NoRoomsToStayOff} = 0; # switch on all rooms if allowed
  759. $hash->{NoRoomsToStayOnThreshold} = 0; # pulse threshold to use "NoRoomsToStayOn"
  760. $hash->{p_minOnOffThreshold} = "";
  761. }
  762. ##########
  763. # [<overallHeatingSwitch>]
  764. if (int(@a) > 8) {
  765. my ($hactor, $h_threshold, $h_followUpTime, $h_regexp_on, $h_delayTimeOn) = split (",", $a[8], 5);
  766. $h_followUpTime = 0 unless ($h_followUpTime);
  767. $h_threshold = 0 unless ($h_threshold);
  768. $h_regexp_on = "on" unless ($h_regexp_on);
  769. $h_delayTimeOn = 0 unless ($h_delayTimeOn);
  770. if (!$defs{$hactor} && $hactor ne "dummy")
  771. {
  772. my $msg = "$name: Unknown actor device $hactor specified";
  773. Log3 ($hash, 3, "PWM_Define $msg");
  774. return $msg;
  775. }
  776. $hash->{OverallHeatingSwitch} = $hactor;
  777. $hash->{OverallHeatingSwitch_threshold} = $h_threshold;
  778. $hash->{OverallHeatingSwitch_regexp_on} = $h_regexp_on;
  779. $hash->{OverallHeatingSwitch_roomBased} = ($h_threshold > 0) ? "off" : "on";
  780. $hash->{OverallHeatingSwitch_followUpTime} = $h_followUpTime;
  781. $hash->{OverallHeatingSwitch_delayTimeOn} = $h_delayTimeOn;
  782. $hash->{p_overallHeatingSwitch} = $a[8];
  783. readingsSingleUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", "", 0);
  784. readingsSingleUpdate ($hash, "OverallHeatingSwitchWaitBeforeOn", "", 0);
  785. readingsSingleUpdate ($hash, "OverallHeatingSwitch", "", 0);
  786. delete ($hash->{READINGS}{OverallHeatingSwitchWaitUntil}) if defined ($hash->{READINGS}{OverallHeatingSwitchWaitUntil});
  787. delete ($hash->{READINGS}{OverallHeatingSwitchWaitBefore}) if defined ($hash->{READINGS}{OverallHeatingSwitchWaitBefore});
  788. } else {
  789. $hash->{OverallHeatingSwitch} = "";
  790. $hash->{OverallHeatingSwitch_threshold} = "";
  791. $hash->{OverallHeatingSwitch_regexp_on} = "";
  792. $hash->{OverallHeatingSwitch_roomBased} = "";
  793. $hash->{OverallHeatingSwitch_followUpTime} = "";
  794. $hash->{OverallHeatingSwitch_delayTimeOn} = "";
  795. $hash->{p_overallHeatingSwitch} = "";
  796. readingsSingleUpdate ($hash, "OverallHeatingSwitchWaitUntilOff", "", 0);
  797. readingsSingleUpdate ($hash, "OverallHeatingSwitchWaitBeforeOn", "", 0);
  798. readingsSingleUpdate ($hash, "OverallHeatingSwitch", "", 0);
  799. delete ($hash->{READINGS}{OverallHeatingSwitchWaitUntil}) if defined ($hash->{READINGS}{OverallHeatingSwitchWaitUntil});
  800. delete ($hash->{READINGS}{OverallHeatingSwitchWaitBefore}) if defined ($hash->{READINGS}{OverallHeatingSwitchWaitBefore});
  801. }
  802. #AssignIoPort($hash);
  803. if($hash->{INTERVAL} > 0) {
  804. InternalTimer(gettimeofday() + 10, "PWM_Calculate", $hash, 0);
  805. }
  806. Log3 ($hash, 3, "PWM Define $name");
  807. return undef;
  808. }
  809. ###################################
  810. sub PWM_Undef($$)
  811. {
  812. my ($hash, $args) = @_;
  813. my $name = $hash->{NAME};
  814. Log3 ($hash, 3, "PWM Undef $name");
  815. if ( $hash->{INTERVAL} )
  816. {
  817. RemoveInternalTimer($hash);
  818. }
  819. return undef;
  820. }
  821. sub
  822. PWM_Attr(@)
  823. {
  824. my @a = @_;
  825. my ($action, $name, $attrname, $attrval) = @a;
  826. my $hash = $defs{$name};
  827. $attrval = "" unless defined ($attrval);
  828. if ($action eq "del")
  829. {
  830. if ($attrname eq "overallHeatingSwitchThresholdTemp")
  831. {
  832. delete ($hash->{OverallHeatingSwitchTT_tsensor} ) if defined ($hash->{OverallHeatingSwitchTT_tsensor});
  833. delete ($hash->{OverallHeatingSwitchTT_reading} ) if defined ($hash->{OverallHeatingSwitchTT_reading});
  834. delete ($hash->{OverallHeatingSwitchTT_t_regexp} ) if defined ($hash->{OverallHeatingSwitchTT_t_regexp});
  835. delete ($hash->{OverallHeatingSwitchTT_maxTemp} ) if defined ($hash->{OverallHeatingSwitchTT_maxTemp});
  836. delete ($hash->{READINGS}{OverallHeatingSwitchTT_Off} ) if defined ($hash->{READINGS}{OverallHeatingSwitchTT_Off});
  837. }
  838. if (defined $attr{$name}{$attrname}) {
  839. delete ($attr{$name}{$attrname});
  840. }
  841. return undef;
  842. }
  843. elsif ($action eq "set")
  844. {
  845. if (defined $attr{$name}{$attrname})
  846. {
  847. }
  848. if ($attrname eq "overallHeatingSwitchThresholdTemp")
  849. {
  850. my ($obj, $temp) = split (",", $attrval, 2);
  851. $temp = 50 unless (defined($temp));
  852. unless ($temp =~ /^(\d[\d\.]+)$/)
  853. {
  854. return "$name: invalid temperature for attribute $attrname ($attrval)";
  855. }
  856. if (defined ($obj))
  857. {
  858. my ($sensor, $reading, $t_regexp) = split (":", $obj, 3);
  859. $reading = "temperature" unless defined ($reading);
  860. $t_regexp = '(\d[\d\.]+)', unless defined ($t_regexp);
  861. if (defined($sensor)) # may be not defined yet
  862. {
  863. $hash->{OverallHeatingSwitchTT_tsensor} = $sensor;
  864. $hash->{OverallHeatingSwitchTT_reading} = $reading;
  865. $hash->{OverallHeatingSwitchTT_t_regexp} = $t_regexp;
  866. $hash->{OverallHeatingSwitchTT_maxTemp} = $temp;
  867. } else {
  868. Log3 ($hash, 2, "invalid temperature reading in attribute overallHeatingSwitchThresholdTemp");
  869. return "$name: invalid value for attribute $attrname ($attrval)";
  870. }
  871. } else {
  872. Log3 ($hash, 2, "invalid value for attribute overallHeatingSwitchThresholdTemp");
  873. return "$name: invalid value for attribute $attrname ($attrval)";
  874. }
  875. }
  876. }
  877. Log3 (undef, 2, "called PWM_Attr($a[0],$a[1],$a[2],<$a[3]>)");
  878. return undef;
  879. }
  880. ###################################
  881. 1;
  882. =pod
  883. =item device
  884. =item summary Device for room temperature control using PWM. See also 93_PWM.pm
  885. =begin html
  886. <a name="PWM"></a>
  887. <h3>PWM</h3>
  888. <ul>
  889. <table>
  890. <tr><td>
  891. The PMW module implements temperature regulation for heating systems only capeable of switching on/off.<br><br>
  892. 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>
  893. PWM defines a calculation unit and depents on objects based on PWMR which define the rooms to be heated.<br>
  894. <br>
  895. </td></tr>
  896. </table>
  897. <b>Define</b>
  898. <ul>
  899. <code>define &lt;name&gt; PWM [&lt;interval&gt;] [&lt;cycletime&gt;] [&lt;minonofftime&gt;] [&lt;maxPulse&gt;] [&lt;maxSwitchOnPerCycle&gt;,&lt;maxSwitchOffPerCycle&gt;] [&lt;roomStayOn&gt;,&lt;roomStayOff&gt;,&lt;stayOnThreshold&gt;] [&lt;overallHeatingSwitch&gt;[,&lt;pulseThreshold&gt;[,&lt;followUpTime&gt;[,&lt;h_regexp_on&gt;[,&lt;delayTimeOn&gt;]]]]]<br></code>
  900. <br>
  901. eg. define fb PWM 60 900 120 1 99,99 0,0,0 pumpactor<br>
  902. <br>
  903. Define a calculation object with the following parameters:<br>
  904. <ul>
  905. <li>interval<br>
  906. Calculate the pulses every <i>interval</i> seconds. Default is 60 seconds.<br>
  907. </li>
  908. <li>cycletime<br>
  909. Timeframe to which the pulses refere to. Default is 900 seconds (=15 Minutes). "valve position" of 100% calculates to "on" for this period.<br>
  910. </li>
  911. <li>minonofftime<br>
  912. Default is 120 seconds.
  913. Floor heating systems are driven by thermomechanic elements which react very slow. on/off status changes for lower periods are ignored.<br>
  914. </li>
  915. <li>maxPulse<br>
  916. Default is 1, which means that a device can be switched on for the full <i>cylcetime</i> period.<br>
  917. For energy saving reasons it may be wanted to prevent situations were all rooms are switched on (high energy usage) and afterwards off.<br>
  918. In this case <i>maxPulse</i> is set to 0.85 (=12:45 minutes) which forces a room with a pulse of 1 (=100%) to be switched off after 12:45 minutes to give another
  919. room the chance to be switched on.
  920. <br>
  921. </li>
  922. <li>maxSwitchOnPerCycle,maxSwitchoffPerCycle<br>
  923. Defaults are 99 for both values. This means that 99 PWMR object can be switched on or off at the same time.<br>
  924. To prevent energy usage peaks followend by "no energy consumption" situations set both values to "1".<br>
  925. This means after the room the the least energy required is switched off the next will be switched off.<br>
  926. Rooms are switched on or off one after the other (in <interval> cycles) and not all at one time.<br>
  927. Waiting times are honored by a addon to the pulse.<br>
  928. <br>
  929. </li>
  930. <li>roomStayOn,roomStayOff,stayOnThreshold<br>
  931. Defauts: <br>
  932. <i>roomStayOn</i> = 0 ... all rooms can be switched off at the same time.<br>
  933. <i>roomStayOff</i> = 0 ... all rooms can be switched on at the same time.<br>
  934. <i>stayOnThreshold</i> = 0 ... no impact.<br>
  935. For energy saving reasons the following may be set: "4,1,0.25". This means:<br>
  936. The room with the least pulse will be kept off (<i>roomsStayOff</i>=1)<br>
  937. If the average pulse for the (<i>roomsStayOn</i>=4) rooms with the most heating required is greater than (<i>stayOnThreshold</i>=0.25) then <i>maxRoomStayOn</i> will be kept in state "on", even if the time for the current pulse is reached.
  938. If the threshold is not reached (not so much heating required) then all rooms can be switched off at the same time.<br>
  939. <br>
  940. </li>
  941. <li>&lt;overallHeatingSwitch&gt[,&lt;pulseThreshold&gt[,&lt;followUpTime&gt;[,&lt;regexp_on&gt;[,&lt;delayTimeOn&gt;]]]]<br>
  942. Universal switch to controll eg. pumps or the heater itself. It will be set to "off" if no heating is required and otherwise "on".<br>
  943. <i>pulseThreshold</i> defines a threshold which is applied to reading <i>pulseMax</i>, <i>pulseSum</i>, <i>pulseAvg</i>, <i>pulseAvg2</i> or <i>pulseAvg3</i> of the PWM object to decide if heating is required. If (calculated pulse > threshold) then actor is set to "on", otherwise "off".<br>
  944. If <i>pulseThreshold</i> is set to 0 (or is not defined) then the decision is based on <i>roomsOn</i>. If (roomsOn > 0) then actor is set to "on", otherwise "off".<br>
  945. <i>followUpTime</i> defines a number of seconds which is used to delay the status change from "on" to "off". This can be used to prevent a toggling switch.<br>
  946. <i>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>
  947. <i>delayTimeOn</i> defines a number of seconds which is used to delay the status change from "off" to "on". This can be used to give the valves time to open before switching..<br>
  948. The pulse used for comparision is defined by attribute <i>overallHeatingSwitchRef</i>. Default is <i>maxPulse</i>.<br>
  949. <br>
  950. </li>
  951. </ul>
  952. <br>
  953. Example:<br>
  954. <br>
  955. <code>define fh PWM</code>
  956. <br>which is equal to<br>
  957. <code>define fh PWM 60 900 120 1 99,99 0,0,0</code>
  958. <br>Energy saving definition might be<br>
  959. <code>define fh PWM 60 900 120 0.85 1,1 4,1,0.25</code>
  960. <br><br>
  961. </ul>
  962. <br>
  963. <b>Set </b>
  964. <ul>
  965. <li>cycletime<br>
  966. Temporary change of parameter <i>cycletime</i>.
  967. </li><br>
  968. <li>interval<br>
  969. Temporary change of parameter <i>interval</i>.
  970. </li><br>
  971. <li>recalc<br>
  972. Cause recalculation that normally appeary every <i>interval</i> seconds.
  973. </li><br>
  974. </ul>
  975. <b>Get</b>
  976. <ul>
  977. <li>status<br>
  978. Retrieve content of variable <i>STATE</i>.
  979. </li><br>
  980. <li>timers<br>
  981. Retrieve values from the readings "timer?_??" from the attached rooms..<br>
  982. The readings define start and end times for different room temperatures.<br>
  983. This funktion will retrieve the first start and the last end time. <i>STATE</i>.
  984. </li><br>
  985. </ul>
  986. <br>
  987. <b>Attributes</b>
  988. <ul>
  989. <li>disable<br>
  990. Set to 1 will disable all calculations and STATE will be set to "disabled".<br>
  991. </li><br>
  992. <li>valveProtectIdlePeriod<br>
  993. Protect Valve by switching on actor for 300 seconds.<br>
  994. After <i>valveProtectIdlePeriod</i> number of days without switching the valve the actor is set to "on" for 300 seconds.
  995. overallHeatingSwitch is not affected.
  996. </li><br>
  997. <li>overallHeatingSwitchRef<br>
  998. Defines which reading is used for threshold comparision for <i>OverallHeatingSwitch</i> calculation. Possible values are:<br>
  999. <i>pulseMax</i>,
  1000. <i>pulseSum</i>,
  1001. <i>pulseAvg</i>,
  1002. <i>pulseAvg2</i>,
  1003. <i>pulseAvg3</i>,
  1004. <i>avgPulseRoomsOn</i><br>
  1005. pulseAvg is an average pulse of all rooms which should be switched to "on".<br>
  1006. pulseAvg2 and pulseAvg3 refer to the 2 or 3 romms with highest pulses.
  1007. </li><br>
  1008. <li>overallHeatingSwitchThresholdTemp<br>
  1009. Defines a reading for a temperature and a maximum value that prevents the overallHeatingSwitch from switching to "on".<br>
  1010. Value has the following format: tsensor[:reading[:t_regexp]],maxValue.<br>
  1011. <i>tsensor</i> defines the temperature sensor for the actual temperature.<br>
  1012. <i>reading</i> defines the reading of the temperature sensor. Default is "temperature"<br>
  1013. <i>t_regexp</i> defines a regular expression to be applied to the reading. Default is '(\d[\d\.]+)'.<br>
  1014. if <i>maxValue</i> is reached as a temperature from tsensor then overallHeatingSwitch will not be switch to "on".<br>
  1015. Example: tsensor,44 or tsensor:temperature,44 or tsensor:temperature:(\d+).*,44<br>
  1016. The reading OverallHeatingSwitchTT_Off will be set to 1 if temperature from tsensor prevents <i>overallHeatingSwitch</i> from switching to "on".<br>
  1017. Please be aware that temperatures raising to high will seriously harm your heating system and this parameter should not be used as the only protection feature.<br>
  1018. Using this parameter is on your own risk. Please test your settings very carefully.<br>
  1019. </li>
  1020. </ul>
  1021. <br>
  1022. </ul>
  1023. =end html
  1024. =cut