59_HCS.pm 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  1. ################################################################
  2. # $Id: 59_HCS.pm 9735 2015-10-31 13:25:24Z rudolfkoenig $
  3. # vim: ts=2:et
  4. #
  5. # (c) 2012 Copyright: Martin Fischer (m_fischer at gmx dot de)
  6. # All rights reserved
  7. #
  8. # This script free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # any later version.
  12. #
  13. # The GNU General Public License can be found at
  14. # http://www.gnu.org/copyleft/gpl.html.
  15. # A copy is found in the textfile GPL.txt and important notices to the license
  16. # from the author is found in LICENSE.txt distributed with these scripts.
  17. #
  18. # This script is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. #
  23. ################################################################
  24. package main;
  25. use strict;
  26. use warnings;
  27. sub HCS_Initialize($$);
  28. sub HCS_Define($$);
  29. sub HCS_DoInit($);
  30. sub HCS_Undef($$);
  31. sub HCS_checkState($);
  32. sub HCS_Get($@);
  33. sub HCS_Set($@);
  34. sub HCS_setState($$);
  35. sub HCS_getValues($$);
  36. my %gets = (
  37. "values" => "",
  38. );
  39. my %sets = (
  40. "interval" => "",
  41. "eco" => "",
  42. "mode" => "",
  43. "on" => "",
  44. "off" => "",
  45. );
  46. my %defaults = (
  47. "idleperiod" => 10,
  48. "interval" => 5,
  49. "deviceCmdOn" => "on",
  50. "deviceCmdOff" => "off",
  51. "ecoTemperatureOn" => 16.0,
  52. "ecoTemperatureOff" => 17.0,
  53. "eventOnChangeReading" => "state,devicestate,eco,overdrive",
  54. "mode" => "thermostat",
  55. "thermostatThresholdOn" => 0.5,
  56. "thermostatThresholdOff" => 0.5,
  57. "valveThresholdOn" => 35,
  58. "valveThresholdOff" => 40,
  59. );
  60. #####################################
  61. sub
  62. HCS_Initialize($$)
  63. {
  64. my ($hash) = @_;
  65. $hash->{DefFn} = "HCS_Define";
  66. $hash->{UndefFn} = "HCS_Undef";
  67. $hash->{GetFn} = "HCS_Get";
  68. $hash->{SetFn} = "HCS_Set";
  69. $hash->{NotifyFn} = "HCS_Notify";
  70. no warnings 'qw';
  71. my @attrList = qw(
  72. deviceCmdOff
  73. deviceCmdOn
  74. disable:0,1
  75. ecoTemperatureOff
  76. ecoTemperatureOn
  77. exclude
  78. idleperiod
  79. interval
  80. mode:thermostat,valve
  81. sensor
  82. sensorReading
  83. sensorThresholdOff
  84. sensorThresholdOn
  85. thermostatThresholdOff
  86. thermostatThresholdOn
  87. valveThresholdOff
  88. valveThresholdOn
  89. );
  90. use warnings 'qw';
  91. $hash->{AttrList} = join(" ", @attrList) ." ". $readingFnAttributes;
  92. }
  93. #####################################
  94. sub
  95. HCS_Define($$) {
  96. my ($hash, $def) = @_;
  97. my $type = $hash->{TYPE};
  98. # define <name> HCS <device>
  99. # define heatingControl HCS KG.hz.LC.SW1.01
  100. my @a = split("[ \t][ \t]*", $def);
  101. return "Wrong syntax: use 'define <name> HCS <device>'"
  102. if(@a < 3 || @a > 6);
  103. my $name = $a[0];
  104. if(!defined($defs{$a[2]})) {
  105. my $ret = "Device $a[2] not defined. Please add this device first!";
  106. Log 1, "$type $name $ret";
  107. return $ret;
  108. }
  109. $hash->{STATE} = "Defined";
  110. readingsSingleUpdate($hash,"device",$a[2],1);
  111. HCS_DoInit($hash);
  112. return undef;
  113. }
  114. #####################################
  115. sub
  116. HCS_Undef($$) {
  117. my ($hash, $name) = @_;
  118. delete($modules{HCS}{defptr}{$hash->{NAME}});
  119. RemoveInternalTimer($hash);
  120. return undef;
  121. }
  122. #####################################
  123. sub
  124. HCS_Notify($$) {
  125. my ($hash,$dev) = @_;
  126. my $name = $hash->{NAME};
  127. my $type = $hash->{TYPE};
  128. return if($dev->{NAME} ne "global" ||
  129. !grep(m/^INITIALIZED$/, @{$dev->{CHANGED}}));
  130. return if($attr{$name} && $attr{$name}{disable});
  131. delete $modules{HCS}{NotifyFn};
  132. delete $hash->{NTFY_ORDER} if($hash->{NTFY_ORDER});
  133. HCS_DoInit($hash);
  134. return undef;
  135. }
  136. #####################################
  137. sub
  138. HCS_DoInit($) {
  139. my ($hash) = @_;
  140. my $name = $hash->{NAME};
  141. my $type = $hash->{TYPE};
  142. # clean upd old stuff
  143. foreach my $r ( keys %{$hash->{READINGS}} ) {
  144. delete $hash->{READINGS}{$r} if($r =~ m/.*_state$/ || $r =~ m/.*_demand$/);
  145. }
  146. $attr{$name}{deviceCmdOn} = AttrVal($name,"deviceCmdOn",$defaults{deviceCmdOn});
  147. $attr{$name}{deviceCmdOff} = AttrVal($name,"deviceCmdOff",$defaults{deviceCmdOff});
  148. $attr{$name}{"event-on-change-reading"} = AttrVal($name,"event-on-change-reading",$defaults{eventOnChangeReading});
  149. $attr{$name}{interval} = AttrVal($name,"interval",$defaults{interval});
  150. $attr{$name}{idleperiod} = AttrVal($name,"idleperiod",$defaults{idleperiod});
  151. $attr{$name}{mode} = AttrVal($name,"mode",$defaults{mode});
  152. if($attr{$name}{mode} ne "thermostat" && $attr{$name}{mode} ne "valve") {
  153. Log3 $name, 1, "$type $name unknown attribute mode '".$attr{$name}{mode}."'. Please use 'thermostat' or 'valve'.";
  154. return undef;
  155. }
  156. $attr{$name}{thermostatThresholdOn} = AttrVal($name,"thermostatThresholdOn",$defaults{thermostatThresholdOn});
  157. $attr{$name}{thermostatThresholdOff} = AttrVal($name,"thermostatThresholdOff",$defaults{thermostatThresholdOff});
  158. $attr{$name}{valveThresholdOn} = AttrVal($name,"valveThresholdOn",$defaults{valveThresholdOn});
  159. $attr{$name}{valveThresholdOff} = AttrVal($name,"valveThresholdOff",$defaults{valveThresholdOff});
  160. readingsSingleUpdate($hash,"state","Initialized",1);
  161. if($init_done) {
  162. my $ret = HCS_getValues($hash,0);
  163. HCS_setState($hash,$ret);
  164. RemoveInternalTimer($hash);
  165. my $timer = gettimeofday()+($attr{$name}{interval}*60);
  166. InternalTimer($timer, "HCS_checkState", $hash, 0);
  167. $hash->{NEXTCHECK} = FmtTime($timer);
  168. }
  169. return undef;
  170. }
  171. #####################################
  172. sub
  173. HCS_checkState($) {
  174. my ($hash) = @_;
  175. my $name = $hash->{NAME};
  176. my $interval = AttrVal($name,"interval",$defaults{interval});
  177. my $timer;
  178. my $ret;
  179. $ret = HCS_getValues($hash,0);
  180. HCS_setState($hash,$ret);
  181. RemoveInternalTimer($hash);
  182. $timer = gettimeofday()+($interval*60);
  183. InternalTimer($timer, "HCS_checkState", $hash, 0);
  184. $hash->{NEXTCHECK} = FmtTime($timer);
  185. return undef;
  186. }
  187. #####################################
  188. sub
  189. HCS_Get($@) {
  190. my ($hash, @a) = @_;
  191. my $name = $hash->{NAME};
  192. my $type = $hash->{TYPE};
  193. my $ret;
  194. # check syntax
  195. return "argument is missing @a"
  196. if(int(@a) != 2);
  197. # check argument
  198. return "Unknown argument $a[1], choose one of ".join(" ", sort keys %gets)
  199. if(!defined($gets{$a[1]}));
  200. # get argument
  201. my $arg = $a[1];
  202. if($arg eq "values") {
  203. $ret = HCS_getValues($hash,1);
  204. return $ret;
  205. }
  206. return undef;
  207. }
  208. #####################################
  209. sub
  210. HCS_Set($@) {
  211. my ($hash, @a) = @_;
  212. my $name = $hash->{NAME};
  213. my $type = $hash->{TYPE};
  214. my $timer;
  215. my $ret;
  216. my $str;
  217. # check syntax
  218. return "argument is missing @a"
  219. if(int(@a) < 2 || int(@a) > 3);
  220. # check argument
  221. return "Unknown argument $a[1], choose one of ".join(" ", sort keys %sets)
  222. if(!defined($sets{$a[1]}));
  223. # get argument
  224. my $arg = $a[1];
  225. if($arg eq "eco") {
  226. return "argument is missing, choose one of on off"
  227. if(int(@a) < 3);
  228. return "Unknown argument $a[2], choose one of on off"
  229. if(lc($a[2]) ne "on" && lc($a[2]) ne "off");
  230. my $ecoModeNew = lc($a[2]);
  231. my $ecoTempOn = AttrVal($name,"ecoTemperatureOn",undef);
  232. my $ecoTempOff = AttrVal($name,"ecoTemperatureOff",undef);
  233. if((!$ecoTempOn || !$ecoTempOff) && $ecoModeNew eq "on") {
  234. $str = "missing attribute 'ecoTemperatureOn'. Please define this attribute first."
  235. if(!$ecoTempOn);
  236. $str = "missing attribute 'ecoTemperatureOff'. Please define this attribute first."
  237. if(!$ecoTempOff);
  238. Log3 $name, 1, "$type $name $str";
  239. return $str;
  240. }
  241. my $ecoModeOld = ReadingsVal($name,"eco","off");
  242. if($ecoModeNew ne $ecoModeOld) {
  243. readingsSingleUpdate($hash, "eco",$ecoModeNew,1);
  244. $str = "eco mode changed from $ecoModeOld to $ecoModeNew";
  245. Log3 $name, 1, "$type $name $str";
  246. return $str;
  247. } else {
  248. return "eco mode '$ecoModeNew' already set.";
  249. }
  250. } elsif($arg eq "interval") {
  251. return "Wrong interval format: Only digits are allowed!"
  252. if($a[2] !~ m/^\d+$/);
  253. my $intervalNew = $a[2];
  254. my $intervalOld = AttrVal($name,"interval",10);
  255. RemoveInternalTimer($hash);
  256. $attr{$name}{interval} = $intervalNew;
  257. $timer = gettimeofday()+($intervalNew*60);
  258. InternalTimer($timer, "HCS_checkState", $hash, 0);
  259. $hash->{NEXTCHECK} = FmtTime($timer);
  260. Log3 $name, 1, "$type $name interval changed from $intervalOld to $intervalNew";
  261. } elsif($arg eq "mode") {
  262. return "argument is missing, choose one of thermostat valve"
  263. if(int(@a) < 3);
  264. return "Unknown argument $a[2], choose one of thermostat valve"
  265. if(lc($a[2]) ne "thermostat" && lc($a[2]) ne "valve");
  266. my $modeNew = $a[2];
  267. my $modeOld = AttrVal($name,"mode","thermostat");
  268. if($modeNew ne $modeOld) {
  269. $attr{$name}{mode} = "thermostat" if(lc($a[2]) eq "thermostat");
  270. $attr{$name}{mode} = "valve" if(lc($a[2]) eq "valve");
  271. $str = "mode changed from $modeOld to $modeNew";
  272. Log3 $name, 1, "$type $name $str";
  273. } else {
  274. return "mode '$modeNew' already set.";
  275. }
  276. } elsif($arg eq "on") {
  277. RemoveInternalTimer($hash);
  278. HCS_checkState($hash);
  279. Log3 $name, 1, "$type $name monitoring of devices started";
  280. } elsif($arg eq "off") {
  281. RemoveInternalTimer($hash);
  282. $hash->{NEXTCHECK} = "offline";
  283. readingsSingleUpdate($hash, "state", "off",1);
  284. Log3 $name, 1, "$type $name monitoring of devices interrupted";
  285. }
  286. }
  287. #####################################
  288. sub
  289. HCS_setState($$) {
  290. my ($hash,$heatDemand) = @_;
  291. my $name = $hash->{NAME};
  292. my $type = $hash->{TYPE};
  293. my $device = ReadingsVal($name,"device","");
  294. my $deviceState = Value($device);
  295. my $deviceCmdOn = AttrVal($name,"deviceCmdOn",$defaults{deviceCmdOn});
  296. my $deviceCmdOff = AttrVal($name,"deviceCmdOff",$defaults{deviceCmdOff});
  297. my $eco = ReadingsVal($name,"eco","off");
  298. my $idlePeriod = AttrVal($name,"idleperiod",$defaults{idleperiod});
  299. my $lastPeriodTime = ($hash->{helper}{lastSentDeviceCmdOn}) ? $hash->{helper}{lastSentDeviceCmdOn} : 0;
  300. my $newPeriodTime = int(gettimeofday());
  301. my $diffPeriodTime = int((int($newPeriodTime)-int($lastPeriodTime))/60);
  302. my $overdrive = "off";
  303. my $idle = 0;
  304. my $wait = "00:00:00";
  305. my $cmd;
  306. my $mode;
  307. my $state;
  308. my $stateDevice;
  309. if($heatDemand == 0) {
  310. $state = "idle";
  311. $cmd = $deviceCmdOff;
  312. } elsif($heatDemand == 1) {
  313. $state = "demand";
  314. $cmd = $deviceCmdOn;
  315. } elsif($heatDemand == 2) {
  316. $eco = "on";
  317. $state = "idle (eco)";
  318. $cmd = $deviceCmdOff;
  319. } elsif($heatDemand == 3) {
  320. $eco = "on";
  321. $state = "demand (eco)";
  322. $cmd = $deviceCmdOn;
  323. } elsif($heatDemand == 4) {
  324. $overdrive = "on";
  325. $state = "idle (overdrive)";
  326. $cmd = $deviceCmdOff;
  327. } elsif($heatDemand == 5) {
  328. $overdrive = "on";
  329. $state = "demand (overdrive)";
  330. $cmd = $deviceCmdOn;
  331. }
  332. my $eventOnChange = AttrVal($name,"event-on-change-reading","");
  333. my $eventOnUpdate = AttrVal($name,"event-on-update-reading","");
  334. $stateDevice = ReadingsVal($name,"devicestate",$defaults{deviceCmdOff});
  335. if($idlePeriod && $diffPeriodTime < $idlePeriod) {
  336. $wait = FmtTime((($idlePeriod-$diffPeriodTime)*60)-3600);
  337. if($heatDemand == 1 || $heatDemand == 3 || $heatDemand == 5 && $cmd eq $deviceCmdOn) {
  338. $idle = 1;
  339. $state = "locked" if($stateDevice eq $deviceCmdOff);
  340. }
  341. }
  342. readingsBeginUpdate($hash);
  343. if(!$defs{$device}) {
  344. $state = "error";
  345. Log3 $name, 1, "$type $name device '$device' does not exists.";
  346. } else {
  347. if($idle == 1 && $cmd eq $deviceCmdOn && $stateDevice ne $deviceCmdOn) {
  348. Log3 $name, 3, "$type $name device $device locked for $wait min.";
  349. } else {
  350. if(!$eventOnChange ||
  351. ($eventOnUpdate && $eventOnUpdate =~ m/devicestate/ ||
  352. $eventOnChange && $eventOnChange =~ m/devicestate/ ) &&
  353. ($cmd ne $stateDevice || $deviceState ne $stateDevice)) {
  354. my $cmdret = CommandSet(undef,"$device $cmd");
  355. if($cmdret) {
  356. Log3 $name, 1, "$type $name An error occurred while switching device '$device': $cmdret";
  357. } else {
  358. readingsBulkUpdate($hash, "devicestate", $cmd);
  359. if($cmd eq $deviceCmdOn) {
  360. $hash->{helper}{lastSentDeviceCmdOn} = $newPeriodTime;
  361. $wait = FmtTime((($idlePeriod)*60)-3600);
  362. }
  363. }
  364. }
  365. }
  366. }
  367. readingsBulkUpdate($hash, "eco", $eco);
  368. readingsBulkUpdate($hash, "locked", $wait);
  369. readingsBulkUpdate($hash, "overdrive", $overdrive);
  370. readingsBulkUpdate($hash, "state", $state);
  371. readingsEndUpdate($hash, 1);
  372. return undef;
  373. }
  374. #####################################
  375. sub
  376. HCS_getValues($$) {
  377. my ($hash,$list) = @_;
  378. my $name = $hash->{NAME};
  379. my $type = $hash->{TYPE};
  380. my %devs = ();
  381. my $exclude = AttrVal($name,"exclude","");
  382. my @lengthNames;
  383. my @lengthValves;
  384. my $ret;
  385. # get devices
  386. foreach my $d (sort keys %defs) {
  387. my $t = $defs{$d}{TYPE};
  388. # skipping unneeded devices
  389. next if($t ne "FHT" && $t ne "CUL_HM" && $t ne "MAX");
  390. next if($t eq "MAX" && !$defs{$d}{type});
  391. next if($t eq "MAX" && $defs{$d}{type} !~ m/HeatingThermostat/);
  392. next if($t eq "CUL_HM" &&( !$attr{$d}{model}
  393. ||!( ($attr{$d}{model} eq "HM-CC-TC" && !$defs{$d}{device})
  394. ||($attr{$d}{model} eq "HM-CC-RT-DN" && !$defs{$d}{device}))));
  395. $devs{$d}{actuator} = ReadingsVal($d,"valveposition","n/a") if($t =~ m/(MAX)/);
  396. $devs{$d}{actuator} = ReadingsVal($d,"actuator","n/a") if($t =~ m/(FHT|CUL_HM)/);
  397. if ($devs{$d}{actuator} =~ m/^\d+\s*%?$/) {
  398. $devs{$d}{actuator} =~ s/(\s+|%)//g;
  399. } else {
  400. $devs{$d}{actuator} = 0;
  401. }
  402. $devs{$d}{excluded} = ($exclude =~ m/$d/) ? 1 : 0;
  403. $devs{$d}{ignored} = ($attr{$d}{ignore} && $attr{$d}{ignore} == 1) ? 1 : 0;
  404. $devs{$d}{tempDesired} = ReadingsVal($d,"desired-temp","n/a") if($t =~ m/(FHT|CUL_HM)/);
  405. $devs{$d}{tempDesired} = ReadingsVal($d,"desiredTemperature","n/a") if($t =~ m/(MAX)/);
  406. $devs{$d}{tempMeasured} = ReadingsVal($d,"measured-temp","n/a") if($t =~ m/(FHT|CUL_HM)/);
  407. $devs{$d}{tempMeasured} = ReadingsVal($d,"temperature","n/a") if($t =~ m/(MAX)/);
  408. $devs{$d}{tempDesired} = ($t =~ m/(FHT)/) ? 5.5 : 4.5 if($devs{$d}{tempDesired} eq "off");
  409. $devs{$d}{tempDesired} = 30.5 if($devs{$d}{tempDesired} eq "on");
  410. $devs{$d}{type} = $t;
  411. $hash->{helper}{device}{$d}{excluded} = $devs{$d}{excluded};
  412. $hash->{helper}{device}{$d}{ignored} = $devs{$d}{ignored};
  413. push(@lengthNames,$d);
  414. push(@lengthValves,$devs{$d}{actuator});
  415. }
  416. my $ln = (reverse sort { $a <=> $b } map { length($_) } @lengthNames)[0];
  417. my $lv = (reverse sort { $a <=> $b } map { length($_) } @lengthValves)[0];
  418. # show list of devices
  419. if($list) {
  420. my $nextCheck = ($hash->{NEXTCHECK}) ? $hash->{NEXTCHECK} : "n/a";
  421. my $delta;
  422. my $str;
  423. foreach my $d (sort keys %{$hash->{helper}{device}}) {
  424. my $info = "";
  425. my $act = ($hash->{helper}{device}{$d}{actuator} eq "n/a") ? " n/a" :
  426. sprintf("%${lv}d",$hash->{helper}{device}{$d}{actuator});
  427. my $td = ($hash->{helper}{device}{$d}{tempDesired} eq "n/a") ? " n/a" :
  428. sprintf("%4.1f",$hash->{helper}{device}{$d}{tempDesired});
  429. my $tm = ($hash->{helper}{device}{$d}{tempMeasured} eq "n/a") ? " n/a" :
  430. sprintf("%4.1f",$hash->{helper}{device}{$d}{tempMeasured});
  431. $info = "idle" if($hash->{helper}{device}{$d}{demand} == 0);
  432. $info = "demand" if($hash->{helper}{device}{$d}{demand} == 1);
  433. $info = "(excluded)" if($hash->{helper}{device}{$d}{excluded} == 1);
  434. $info = "(ignored)" if($hash->{helper}{device}{$d}{ignored} == 1);
  435. if($td eq " n/a" || $tm eq " n/a") {
  436. $delta = " n/a";
  437. } else {
  438. $delta = sprintf(" %.1f",$td-$tm);
  439. $delta = sprintf("+%.1f",$tm-$td) if($tm > $td);
  440. $delta = sprintf("-%.1f",$td-$tm) if($td > $tm);
  441. }
  442. $str .= sprintf("%-${ln}s: desired: %s°C measured: %s°C delta: %s valve: %${lv}d%% state: %s\n",
  443. $d,$td,$tm,$delta,$act,$info);
  444. }
  445. $str .= "next check: $nextCheck\n";
  446. $ret = $str;
  447. return $ret;
  448. }
  449. # housekeeping
  450. foreach my $d (sort keys %{$hash->{helper}{device}}) {
  451. delete $hash->{helper}{device}{$d} if(!exists $devs{$d});
  452. }
  453. # reset counter
  454. my $sumDemand = 0;
  455. my $sumExcluded = 0;
  456. my $sumFHT = 0;
  457. my $sumHMCCTC = 0;
  458. my $sumMAX = 0;
  459. my $sumIdle = 0;
  460. my $sumIgnored = 0;
  461. my $sumTotal = 0;
  462. my $sumUnknown = 0;
  463. my $mode = AttrVal($name,"mode",$defaults{mode});
  464. readingsBeginUpdate($hash);
  465. foreach my $d (sort keys %devs) {
  466. my $devState;
  467. $hash->{helper}{device}{$d}{actuator} = $devs{$d}{actuator};
  468. $hash->{helper}{device}{$d}{excluded} = $devs{$d}{excluded};
  469. $hash->{helper}{device}{$d}{ignored} = $devs{$d}{ignored};
  470. $hash->{helper}{device}{$d}{tempDesired} = $devs{$d}{tempDesired};
  471. $hash->{helper}{device}{$d}{tempMeasured} = $devs{$d}{tempMeasured};
  472. $hash->{helper}{device}{$d}{type} = $devs{$d}{type};
  473. $sumMAX++ if(lc($devs{$d}{type}) eq "max");
  474. $sumFHT++ if(lc($devs{$d}{type}) eq "fht");
  475. $sumHMCCTC++ if(lc($devs{$d}{type}) eq "cul_hm");
  476. $sumTotal++;
  477. if($devs{$d}{ignored}) {
  478. $devState = "ignored";
  479. $hash->{helper}{device}{$d}{demand} = 0;
  480. readingsBulkUpdate($hash,$d,$devState);
  481. Log3 $name, 4, "$type $name $d: $devState";
  482. $sumIgnored++;
  483. next;
  484. }
  485. if($devs{$d}{excluded}) {
  486. $devState = "excluded";
  487. $hash->{helper}{device}{$d}{demand} = 0;
  488. readingsBulkUpdate($hash,$d,$devState);
  489. Log3 $name, 4, "$type $name $d: $devState";
  490. $sumExcluded++;
  491. next;
  492. }
  493. if($mode eq "thermostat" && ($devs{$d}{tempMeasured} eq "n/a" || $devs{$d}{tempDesired} eq "n/a")) {
  494. $devState = "unknown";
  495. $hash->{helper}{device}{$d}{demand} = 0;
  496. readingsBulkUpdate($hash,$d,$devState);
  497. Log3 $name, 4, "$type $name $d: $devState";
  498. $sumUnknown++;
  499. next;
  500. }
  501. my $lastState = ReadingsVal($name,$d,"idle");
  502. my $act = $devs{$d}{actuator};
  503. my $tm = $devs{$d}{tempMeasured};
  504. my $td = $devs{$d}{tempDesired};
  505. my $delta;
  506. my $str;
  507. if(!$hash->{helper}{device}{$d}{demand}) {
  508. $hash->{helper}{device}{$d}{demand} = 0;
  509. $lastState = "idle";
  510. }
  511. if($mode eq "thermostat") {
  512. my $tOn = AttrVal($name,"thermostatThresholdOn",$defaults{thermostatThresholdOn});
  513. my $tOff = AttrVal($name,"thermostatThresholdOff",$defaults{thermostatThresholdOff});
  514. if($tm > $td && $tm-$td >= $tOff) {
  515. $devState = "idle";
  516. $hash->{helper}{device}{$d}{demand} = 0;
  517. $delta = $tm-$td;
  518. $str = sprintf("desired: %4.1f measured: %4.1f delta: +%.1f open: %${lv}d%% state: %s",$td,$tm,$delta,$act,$devState);
  519. $sumIdle++;
  520. } elsif($td > $tm && $td-$tm >= $tOn) {
  521. $devState = "demand";
  522. $hash->{helper}{device}{$d}{demand} = 1;
  523. $delta = $td-$tm;
  524. $str = sprintf("desired: %4.1f measured: %4.1f delta: -%.1f open: %${lv}d%% state: %s",$td,$tm,$delta,$act,$devState);
  525. $sumDemand++;
  526. } elsif($tm > $td) {
  527. $devState = $lastState;
  528. $delta = $tm-$td;
  529. $str = sprintf("desired: %4.1f measured: %4.1f delta: +%.1f open: %${lv}d%% state: %s",$td,$tm,$delta,$act,$devState);
  530. $sumIdle++ if($devState eq "idle");
  531. $sumDemand++ if($devState eq "demand");
  532. } elsif($td > $tm) {
  533. $devState = $lastState;
  534. $delta = $td-$tm;
  535. $str = sprintf("desired: %4.1f measured: %4.1f delta: -%.1f open: %${lv}d%% state: %s",$td,$tm,$delta,$act,$devState);
  536. $sumIdle++ if($devState eq "idle");
  537. $sumDemand++ if($devState eq "demand");
  538. } else {
  539. $devState = $lastState;
  540. $delta = $td-$tm;
  541. $str = sprintf("desired: %4.1f measured: %4.1f delta: %.1f open: %${lv}d%% state: %s",$td,$tm,$delta,$act,$devState);
  542. $sumIdle++ if($devState eq "idle");
  543. $sumDemand++ if($devState eq "demand");
  544. }
  545. } elsif($mode eq "valve") {
  546. my $vOn = AttrVal($name,"valveThresholdOn",$defaults{valveThresholdOn});
  547. my $vOff = AttrVal($name,"valveThresholdOff",$defaults{valveThresholdOff});
  548. my $valve = $devs{$d}{actuator};
  549. if($valve >= $vOn) {
  550. $devState = "demand";
  551. $hash->{helper}{device}{$d}{demand} = 1;
  552. $sumDemand++;
  553. } else {
  554. if($lastState eq "demand") {
  555. if($valve > $vOff) {
  556. $devState = "demand";
  557. $hash->{helper}{device}{$d}{demand} = 1;
  558. $sumDemand++;
  559. } else {
  560. $devState = "idle";
  561. $hash->{helper}{device}{$d}{demand} = 0;
  562. $sumIdle++;
  563. }
  564. } else {
  565. $devState = "idle";
  566. $hash->{helper}{device}{$d}{demand} = 0;
  567. $sumIdle++;
  568. }
  569. }
  570. $delta = sprintf(" %.1f",$td-$tm);
  571. $delta = sprintf("+%.1f",$tm-$td) if($tm > $td);
  572. $delta = sprintf("-%.1f",$td-$tm) if($td > $tm);
  573. $str = sprintf("desired: %4.1f measured: %4.1f delta: %s valve: %${lv}d%% state: %s",$td,$tm,$delta,$valve,$devState);
  574. }
  575. Log3 $name, 4, "$type $name $d: $str";
  576. readingsBulkUpdate($hash,$d,$devState);
  577. }
  578. readingsEndUpdate($hash,1);
  579. my $heatDemand = 0;
  580. foreach my $d (sort keys %{$hash->{helper}{device}}) {
  581. $heatDemand = 1 if($hash->{helper}{device}{$d}{demand} && $hash->{helper}{device}{$d}{demand} == 1);
  582. }
  583. # eco mode
  584. my $eco = "no";
  585. my $ecoTempOn = AttrVal($name,"ecoTemperatureOn",undef);
  586. my $ecoTempOff = AttrVal($name,"ecoTemperatureOff",undef);
  587. my $ecoState = ReadingsVal($name,"eco","off");
  588. if($ecoState eq "on" && (!$ecoTempOn || !$ecoTempOff)) {
  589. Log3 $name, 1, "$type $name missing attribute 'ecoTemperatureOn'. Please define this attribute first."
  590. if(!$ecoTempOn);
  591. Log3 $name, 1, "$type $name missing attribute 'ecoTemperatureOff'. Please define this attribute first."
  592. if(!$ecoTempOff);
  593. } elsif($ecoState eq "on") {
  594. foreach my $d (sort keys %{$hash->{helper}{device}}) {
  595. my $ignore = $hash->{helper}{device}{$d}{ignored};
  596. my $exclude = $hash->{helper}{device}{$d}{excluded};
  597. my $tempMeasured = $hash->{helper}{device}{$d}{tempMeasured};
  598. next if($tempMeasured eq "n/a");
  599. if(!$ignore && !$exclude) {
  600. $heatDemand = 2 if($tempMeasured >= $ecoTempOff && $heatDemand != 3);
  601. $heatDemand = 3 if($tempMeasured <= $ecoTempOn);
  602. $eco = "yes" if($heatDemand == 2 || $heatDemand == 3);
  603. }
  604. }
  605. }
  606. # overdrive mode
  607. my $overdrive = "no";
  608. my $sensor = AttrVal($name,"sensor",undef);
  609. my $sReading = AttrVal($name,"sensorReading",undef);
  610. my $sTresholdOn = AttrVal($name,"sensorThresholdOn",undef);
  611. my $sTresholdOff = AttrVal($name,"sensorThresholdOff",undef);
  612. if(!$sensor) {
  613. delete $hash->{READINGS}{sensor} if($hash->{READINGS}{sensor});
  614. } else {
  615. Log3 $name, 1, "$type $name Device $sensor not defined. Please add this device first!"
  616. if(!defined($defs{$sensor}));
  617. Log3 $name, 1, "$type $name missing attribute 'sensorReading'. Please define this attribute first."
  618. if(!$sReading);
  619. Log3 $name, 1, "$type $name missing attribute 'sensorThresholdOn'. Please define this attibute first."
  620. if(!$sTresholdOn);
  621. Log3 $name, 1, "$type $name missing attribute 'sensorThresholdOff'. Please define this attribute first."
  622. if(!$sTresholdOff);
  623. if($defs{$sensor} && $sReading && $sTresholdOn && $sTresholdOff) {
  624. my $tValue = ReadingsVal($sensor,$sReading,"n/a");
  625. if($tValue eq "n/a" || $tValue !~ m/^.*\d+.*$/) {
  626. Log3 $name, 1, "$type $name Device $sensor has no valid value.";
  627. } else {
  628. $tValue =~ s/(\s|°|[A-Z]|[a-z])+//g;
  629. $heatDemand = 4 if($tValue >= $sTresholdOff);
  630. $heatDemand = 5 if($tValue <= $sTresholdOn);
  631. $overdrive = "yes" if($heatDemand == 4 || $heatDemand == 5);
  632. readingsSingleUpdate($hash,"sensor",$tValue,1);
  633. }
  634. }
  635. }
  636. my $str = sprintf("Found %d Device(s): %d FHT, %d HM-CC-TC, %d MAX, demand: %d, idle: %d, ignored: %d, excluded: %d, unknown: %d",
  637. $sumTotal,$sumFHT,$sumHMCCTC, $sumMAX, $sumDemand,$sumIdle,$sumIgnored,$sumExcluded,$sumUnknown);
  638. Log3 $name, 3, "$type $name $str, eco: $eco overdrive: $overdrive";
  639. return $heatDemand;
  640. }
  641. 1;
  642. =pod
  643. =begin html
  644. <a name="HCS"></a>
  645. <h3>HCS</h3>
  646. <ul>
  647. Defines a virtual device for monitoring thermostats (FHT, HM-CC-TC, MAX) to control a central
  648. heating unit.<br><br>
  649. <a name="HCSdefine"></a>
  650. <b>Define</b>
  651. <ul>
  652. <code>define &lt;name&gt; HCS &lt;device&gt;</code>
  653. <br><br>
  654. <ul>
  655. <li><code>&lt;device&gt;</code> the name of a predefined device to switch.</li>
  656. </ul>
  657. <br>
  658. The HCS (heating control system) device monitors the state of all detected thermostats
  659. in a free definable interval (by default: 10 min).
  660. <br><br>
  661. Regulation for heating requirement or suppression of the request can be controlled by
  662. valve position or measured temperature (default) using also free definable thresholds.
  663. In doing so, the HCS device also includes the hysteresis between two states.
  664. <br><br>
  665. Example for monitoring measured temperature:
  666. <ul>
  667. Threshold temperature for heating requirement: 0.5 (default)<br>
  668. Threshold temperature for idle: 0.5 (default)<br>
  669. <br>
  670. Heating is required when the measured temperature of a thermostat is lower than
  671. 0.5&deg; Celsius as the desired temperature. HCS then activates the defined device
  672. until the measured temperature of the thermostat is 0.5&deg; Celsius higher as the
  673. desired temperature (threshold for idle). In this example, both tresholds are equal.
  674. </ul>
  675. <br>
  676. Example for monitoring valve position:
  677. <ul>
  678. Threshold valve position for heating requirement: 40% (default)<br>
  679. Threshold valve position for idle: 35% (default)<br>
  680. <br>
  681. Heating is required when the "open" position of a valve is more than 40%. HCS then
  682. activates the defined device until the "open" position of the valve has lowered to
  683. 35% or less (threshold for idle).
  684. </ul>
  685. <br>
  686. The HCS device supports an optional eco mode. The threshold oriented regulation by
  687. measured temperature or valve position can be overridden by setting economic thresholds.
  688. <br><br>
  689. Example:
  690. <ul>
  691. Threshold temperature economic mode on: 15&deg; Celsius<br>
  692. Threshold temperature economic mode off: 25&deg; Celsius<br>
  693. <br>
  694. HCS activates the defined device until the measured temperature of one ore more
  695. thermostats is lower or equal than 15&deg; Celsius. If a measured temperature of one
  696. or more thermostats is higher or equal than 25&deg; Celsius, HCS switch of the defined
  697. device (if none of the measured temperatures of all thermostats is lower or equal as
  698. 15&deg; Celsius).
  699. </ul>
  700. <br>
  701. In addition, the HCS device supports an optional temp-sensor. The threshold and economic
  702. oriented regulation can be overriden by the reading of the temp-sensor (overdrive mode).
  703. <br><br>
  704. Example:
  705. <ul>
  706. Threshold temperature reading for heating requirement: 10&deg; Celsius<br>
  707. Threshold temperature reading for idle: 18&deg; Celsius<br>
  708. <br>
  709. Is a measured temperature ore valve position reaching or exceeding the threshold for
  710. heating requirement, but the temperature reading is more than 18&deg; Celcius, the
  711. selected device will stay deactivated. The measured temperature or valve-position
  712. oriented regulation has been overridden by the temperature reading in this example.
  713. </ul>
  714. <br>
  715. The HCS device automatically detects devices which are ignored. Furthermore, certain
  716. devices can also be excluded of the monitoring manually.
  717. <br><br>
  718. To reduce the transmission load, use the attribute event-on-change-reading, e.g.
  719. <code>attr &lt;name&gt; event-on-change-reading state,devicestate,eco,overdrive</code>
  720. <br><br>
  721. To avoid frequent switching "on" and "off" of the device, a timeout (in minutes) can be set
  722. using the attribute <code>idleperiod</code>.
  723. <br><br>
  724. <a name="HCSget"></a>
  725. <b>Get </b>
  726. <ul>
  727. <li><code>values</code><br>
  728. returns the actual values of each device
  729. </li>
  730. </ul>
  731. <br>
  732. <a name="HCSset"></a>
  733. <b>Set</b>
  734. <ul>
  735. <li><code>eco &lt;on&gt;|&lt;off&gt;</code><br>
  736. enable (<code>on</code>) or disable (<code>off</code>) the economic mode.
  737. </li>
  738. <li><code>interval &lt;value&gt;</code><br>
  739. <code>value</code> modifies the interval of reading the actual valve positions.
  740. The unit is minutes.
  741. </li>
  742. <li><code>mode &lt;thermostat&gt;|&lt;valve&gt;</code><br>
  743. changes the operational mode:<br>
  744. <code>thermostat</code> controls the heating demand by defined temperature
  745. thresholds.<br>
  746. <code>valve</code> controls the heating demand by defined valve position thresholds.
  747. </li>
  748. <li><code>on</code><br>
  749. restarts the monitoring after shutdown by <code>off</code> switch.<br>
  750. HCS device starts up automatically upon FHEM start or after new device implementation!
  751. </li>
  752. <li><code>off</code><br>
  753. shutdown of monitoring, can be restarted by using the <code>on</code> command.
  754. </li>
  755. </ul>
  756. <br>
  757. <a name="HCSattr"></a>
  758. <b>Attributes</b>
  759. <ul>
  760. <li><code>deviceCmdOn</code> (mandatory)<br>
  761. command to activate the device, e.g. <code>on</code>.
  762. Default value: <code>on</code>
  763. </li>
  764. <li><code>deviceCmdOff</code> (mandatory)<br>
  765. command to deactivate the device, e.g. <code>off</code>.
  766. Default value: <code>off</code>
  767. </li>
  768. <li><code>ecoTemperatureOn</code> (Required by <code>eco</code> mode)<br>
  769. defines threshold for measured temperature upon which device is allways switched on
  770. </li>
  771. <li><code>ecoTemperatureOff</code> (Required by <code>eco</code> mode)<br>
  772. defines threshold for measured temperature upon which device is switched off
  773. </li>
  774. <li><code>exclude</code> (optional)<br>
  775. space or comma separated list of devices (FHT or HM-CC-TC) for excluding from
  776. monitoring
  777. </li>
  778. <li><code>idleperiod</code> (mandatory)<br>
  779. locks the device to be switched for the specified period. The unit is minutes.
  780. Default value: <code>10</code>
  781. </li>
  782. <li><code>mode</code> (mandatory)<br>
  783. defines the operational mode:<br>
  784. <code>thermostat</code> controls the heating demand by defined temperature
  785. thresholds.<br>
  786. <code>valve</code> controls the heating demand by defined valve position thresholds.<br>
  787. Default value: <code>thermostat</code>
  788. </li>
  789. <li><code>sensor</code> (optional)<br>
  790. device name of the temp-sensor
  791. </li>
  792. <li><code>sensorThresholdOn</code> (Required by <code>sensor</code>)<br>
  793. threshold for temperature reading activating the defined device
  794. Must be set if <code>sensor</code> has been defined
  795. </li>
  796. <li><code>sensorThresholdOff</code> (Required by <code>sensor</code>)<br>
  797. threshold for temperature reading deactivating the defined device.
  798. Must be set if <code>sensor</code> has been defined
  799. </li>
  800. <li><code>sensorReading</code> (Required by <code>sensor</code>)<br>
  801. name which is used for saving the "reading" of the defined temp-sensor.
  802. </li>
  803. <li><code>thermostatThresholdOn</code> (Required by operational mode <code>thermostat</code>)<br>
  804. defines delta threshold between desired and measured temperature upon which device
  805. is switched on (heating required).<br>
  806. Default value: <code>0.5</code>
  807. </li>
  808. <li><code>thermostatThresholdOff</code> (Required by operational mode <code>thermostat</code>)<br>
  809. defines delta threshold between desired and measured temperature upon which
  810. device is switched off (idle).<br>
  811. Default value: <code>0.5</code>
  812. </li>
  813. <li><code>valveThresholdOn</code> (Required by operational mode <code>valve</code>)<br>
  814. defines threshold of valve-position upon which device is switched on (heating
  815. required).<br>
  816. Default value: <code>40</code>
  817. </li>
  818. <li><code>valveThresholdOff</code> (Required by operational mode <code>valve</code>)<br>
  819. defines threshold of valve-position upon which device is switched off (idle).<br>
  820. Default value: <code>35</code>
  821. </li>
  822. <li><a href="#disable"><code>disable</code></a></li>
  823. <li><a href="#do_not_notify"><code>do_not_notify</code></a></li>
  824. <li><a href="#event-on-change-reading"><code>event-on-change-reading</code></a><br>
  825. default value: <code>state,devicestate,eco,overdrive</code>
  826. </li>
  827. <li><a href="#event-on-update-reading"><code>event-on-update-reading</code></a></li>
  828. <li><a href="#verbose"><code>verbose</code></a><br>
  829. verbose 4 (or lower) shows a complete statistic of scanned devices (FHT or HM-CC-TC).<br>
  830. verbose 3 shows a short summary of scanned devices.<br>
  831. verbose 2 suppressed the above messages.
  832. </li>
  833. </ul>
  834. <br>
  835. </ul>
  836. <br>
  837. </ul>
  838. =end html
  839. =cut