32_TechemHKV.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. ###############################################################################
  2. # $Id: 32_TechemHKV.pm 15731 2017-12-30 21:10:18Z herrmannj $
  3. #
  4. # this module is part of fhem under the same license
  5. # copyright 2015, joerg herrmann
  6. #
  7. # history
  8. # initial checkin
  9. #
  10. ###############################################################################
  11. package main;
  12. use strict;
  13. use warnings;
  14. use Time::HiRes qw(time);
  15. my %typeText = (
  16. '80' => 'Funkheizkostenverteiler data III'
  17. );
  18. sub
  19. TechemHKV_Initialize(@) {
  20. my ($hash) = @_;
  21. # require "Broker.pm";
  22. # TECHEM HKV
  23. # 61, 64 without T1 and T2
  24. $hash->{Match} = "^b..446850[\\d]{8}(61|64|69|94)80.*";
  25. $hash->{DefFn} = "TechemHKV_Define";
  26. $hash->{UndefFn} = "TechemHKV_Undef";
  27. $hash->{SetFn} = "TechemHKV_Set";
  28. $hash->{GetFn} = "TechemHKV_Get";
  29. $hash->{NotifyFn} = "TechemHKV_Notify";
  30. $hash->{ParseFn} = "TechemHKV_Parse";
  31. $hash->{AttrList} = "".$readingFnAttributes;
  32. return undef;
  33. }
  34. sub
  35. TechemHKV_Define(@) {
  36. my ($hash, $def) = @_;
  37. my ($name, $t, $id);
  38. ($name, $t, $id, $def) = split(/ /, $def,4);
  39. return "ID must have 4 or 8 digits" if ($id !~ /^\d{4}(?:\d{4})?$/);
  40. $modules{TechemHKV}{defptr}{$id} = $hash;
  41. $hash->{FRIENDLY} = $def if (defined($def));
  42. $hash->{LONGID} = $id if (length($id) == 8);
  43. # create crc table if required
  44. $data{WMBUS}{crc_table_13757} = TechemHKV_createCrcTable() unless (exists($data{WMBUS}{crc_table_13757}));
  45. # subscribe broadcast channels
  46. # TechemHKV_subscribe($hash, 'foo');
  47. # TechemHKV_Parse($hash, 'b334468500180560094804C3AA20F9F211202B038E80411FD0B81104E6D6265006554261A1B000000000000000001DCBC1706085875BCFADDBEC0F25480');
  48. TechemHKV_Run($hash) if $init_done;
  49. return undef;
  50. }
  51. sub
  52. TechemHKV_Undef(@) {
  53. my ($hash) = @_;
  54. return undef;
  55. };
  56. sub
  57. TechemHKV_Set(@) {
  58. my ($hash, $name, $cmd, @args) = @_;
  59. my $cnt = @args;
  60. return undef;
  61. };
  62. sub
  63. TechemHKV_Get(@) {
  64. my ($hash) = @_;
  65. return undef;
  66. };
  67. sub
  68. TechemHKV_Notify (@) {
  69. my ($hash, $ntfyDev) = @_;
  70. return unless (($ntfyDev->{TYPE} =~ /CUL|STACKABLE/) || ($ntfyDev->{TYPE} eq 'Global'));
  71. foreach my $event (@{$ntfyDev->{CHANGED}}) {
  72. my @e = split(' ', $event);
  73. next unless defined($e[0]);
  74. TechemHKV_Run($hash) if ($e[0] eq 'INITIALIZED');
  75. # patch CUL.pm
  76. TechemHKV_IOPatch($hash, $e[1]) if (($e[0] eq 'ATTR') && ($e[2] eq 'rfmode') && ($e[3] eq 'WMBus_T'));
  77. # disable receiver
  78. if (($e[0] eq 'ATTR') && ($e[2] eq 'rfmode') && ($e[3] ne 'WMBus_T')) {
  79. readingsBeginUpdate($hash);
  80. readingsBulkUpdate($hash, "state", "standby (IO missing)", 1);
  81. readingsBulkUpdate($hash, "temp1", "--.--") if exists($hash->{READINGS}->{'temp1'}); # exlude versions without t1,t2
  82. readingsBulkUpdate($hash, "temp2", "--.--") if exists($hash->{READINGS}->{'temp2'});
  83. readingsEndUpdate($hash, 1);
  84. };
  85. };
  86. return undef;
  87. };
  88. sub
  89. TechemHKV_Receive(@) {
  90. my ($hash, $msg) = @_;
  91. $hash->{LONGID} = $msg->{long} unless defined($hash->{LONGID});
  92. # TODO log collision if any ...
  93. my @t = localtime(time);
  94. my ($ats, $ts);
  95. $hash->{VERSION} = $msg->{version};
  96. $hash->{METER} = $typeText{$msg->{type}};
  97. delete $hash->{CHANGETIME}; # clean up, workaround for fhem prior http://forum.fhem.de/index.php/topic,47474.msg391964.html#msg391964
  98. if (($msg->{version} || '') =~ /69|94/) {
  99. readingsBeginUpdate($hash);
  100. readingsBulkUpdate($hash, "temp1", $msg->{temp1});
  101. readingsBulkUpdate($hash, "temp2", $msg->{temp2});
  102. readingsEndUpdate($hash, 1);
  103. };
  104. # day period changed
  105. $ats = ReadingsTimestamp($hash->{NAME},"current_period", "0");
  106. $ts = sprintf ("%02d-%02d-%02d 00:00:00", $msg->{actual}->{year}, $msg->{actual}->{month}, $msg->{actual}->{day});
  107. if ($ats ne $ts) {
  108. my $i;
  109. readingsBeginUpdate($hash);
  110. $hash->{".updateTimestamp"} = $ts;
  111. $i = $#{ $hash->{CHANGED} };
  112. readingsBulkUpdate($hash, "current_period", $msg->{actualVal});
  113. $hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts if ($#{ $hash->{CHANGED} } != $i ); # only add ts if there is a event to
  114. readingsEndUpdate($hash, 1);
  115. };
  116. # billing period changed
  117. $ats = ReadingsTimestamp($hash->{NAME},"previous_period", "0");
  118. $ts = sprintf ("20%02d-%02d-%02d 00:00:00", $msg->{last}->{year}, $msg->{last}->{month}, $msg->{last}->{day});
  119. if ($ats ne $ts) {
  120. my $i;
  121. readingsBeginUpdate($hash);
  122. $hash->{".updateTimestamp"} = $ts;
  123. $i = $#{ $hash->{CHANGED} };
  124. readingsBulkUpdate($hash, "previous_period", $msg->{lastVal});
  125. $hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts if ($#{ $hash->{CHANGED} } != $i ); # only add ts if there is a event to
  126. readingsEndUpdate($hash, 1);
  127. };
  128. return undef;
  129. };
  130. sub
  131. TechemHKV_Run(@) {
  132. my ($hash) = @_;
  133. # find a CUL
  134. foreach my $d (keys %defs) {
  135. # live patch CUL.pm
  136. TechemHKV_IOPatch($hash, $d) if ($defs{$d}{TYPE} =~ /CUL|STACKABLE/);
  137. }
  138. return undef;
  139. }
  140. # live patch CUL.pm, aka THE HACK
  141. sub
  142. TechemHKV_IOPatch(@) {
  143. my ($hash, $iodev) = @_;
  144. return undef unless (AttrVal($iodev, "rfmode", '') eq "WMBus_T");
  145. # see if already patched
  146. readingsSingleUpdate($hash, "state", "listening", 1);
  147. return undef if ($defs{$iodev}{Clients} =~ /TechemHKV/ );
  148. $defs{$iodev}{Clients} = ":TechemHKV".$defs{$iodev}{Clients};
  149. $defs{$iodev}{'.clientArray'} = undef;
  150. return undef;
  151. }
  152. sub
  153. TechemHKV_Parse(@) {
  154. my ($iohash, $msg) = @_;
  155. my ($message, $rssi);
  156. ($msg, $rssi) = split (/::/, $msg);
  157. $msg = TechemHKV_SanityCheck($msg);
  158. return ('') unless $msg;
  159. $message->{long} = join '', reverse split /(..)/, substr $msg, 6, 8;
  160. $message->{short} = substr $message->{long}, 4, 4;
  161. $message->{version} = substr $msg, 14, 2;
  162. $message->{type} = substr $msg, 16, 2;
  163. # last_date
  164. #if ($message->{version} eq '94') {
  165. # ($message->{last}->{year}, $message->{last}->{month}, $message->{last}->{day})
  166. # = TechemHKV_ParseLastDate(join '', reverse split /(..)/, substr $msg, 24, 4);
  167. #} else {
  168. ($message->{last}->{year}, $message->{last}->{month}, $message->{last}->{day})
  169. = TechemHKV_ParseLastDate(join '', reverse split /(..)/, substr $msg, 22, 4);
  170. #}
  171. # previous_period
  172. #if ($message->{version} eq '94') {
  173. # $message->{lastVal} = hex(join '', reverse split /(..)/, substr $msg, 28, 4);
  174. #} else {
  175. $message->{lastVal} = hex(join '', reverse split /(..)/, substr $msg, 26, 4);
  176. #}
  177. # actual_date
  178. #if ($message->{version} eq '94') {
  179. # ($message->{actual}->{year}, $message->{actual}->{month}, $message->{actual}->{day})
  180. # = TechemHKV_ParseActualDate(join '', reverse split /(..)/, substr $msg, 32, 4);
  181. #} else {
  182. ($message->{actual}->{year}, $message->{actual}->{month}, $message->{actual}->{day})
  183. = TechemHKV_ParseActualDate(join '', reverse split /(..)/, substr $msg, 30, 4);
  184. #}
  185. # actual_period
  186. #if ($message->{version} eq '94') {
  187. # $message->{actualVal} = hex(join '', reverse split /(..)/, substr $msg, 36, 4);
  188. #} else {
  189. $message->{actualVal} = hex(join '', reverse split /(..)/, substr $msg, 34, 4);
  190. #}
  191. # temp sensor 1
  192. if ($message->{version} eq '94') {
  193. $message->{temp1} = sprintf "%.2f", (hex(join '', reverse split /(..)/, substr $msg, 40, 4) / 100);
  194. } elsif ($message->{version} eq '69') {
  195. $message->{temp1} = sprintf "%.2f", (hex(join '', reverse split /(..)/, substr $msg, 38, 4) / 100);
  196. }
  197. # temp sensor 2
  198. if ($message->{version} eq '94') {
  199. $message->{temp2} = sprintf "%.2f", (hex(join '', reverse split /(..)/, substr $msg, 44, 4) / 100);
  200. } elsif ($message->{version} eq '69') {
  201. $message->{temp2} = sprintf "%.2f", (hex(join '', reverse split /(..)/, substr $msg, 42, 4) / 100);
  202. }
  203. # dispatch
  204. if (exists($modules{TechemHKV}{defptr}{$message->{long}})) {
  205. my $deviceHash = $modules{TechemHKV}{defptr}{$message->{long}};
  206. TechemHKV_Receive($deviceHash, $message);
  207. return ($deviceHash->{NAME});
  208. } elsif (exists($modules{TechemHKV}{defptr}{$message->{short}})) {
  209. my $deviceHash = $modules{TechemHKV}{defptr}{$message->{short}};
  210. $modules{TechemHKV}{defptr}{$message->{long}} = $deviceHash;
  211. delete($modules{TechemHKV}{defptr}{$message->{short}});
  212. TechemHKV_Receive($deviceHash, $message);
  213. return ($deviceHash->{NAME});
  214. }
  215. # broadcast
  216. return ('');
  217. }
  218. sub
  219. TechemHKV_SanityCheck(@) {
  220. my ($msg) = @_;
  221. my $rssi;
  222. my $t;
  223. my $dbg = 4;
  224. #($msg, $rssi) = split (/::/, $msg);
  225. my @m = ((substr $msg,1) =~ m/../g);
  226. # at least 3 chars
  227. if (length($msg) < 3) {
  228. Log3 ("TechemHKV", $dbg, "msg incomplete $msg");
  229. return undef;
  230. }
  231. # msg length without crc blocks
  232. my $l = hex(substr $msg, 1, 2) + 1;
  233. # full crc payload blocks
  234. my $fb = int(($l - 10) / 16);
  235. # remaining bytes ?
  236. my $rb = ($l - 10) % 16;
  237. # required len
  238. my $rl = $l + 2 + ($fb * 2) + (($rb)?2:0);
  239. if (($rl * 2) > (length($msg) -1)) {
  240. Log3 ("TechemHKV", $dbg, "msg incomplete $msg");
  241. return undef;
  242. }
  243. # CRC first 10 byte, then chunks of 16 byte then remaining
  244. if ((substr $msg, 21, 4) ne TechemHKV_crc16_13757(substr $msg, 1, 20)) {
  245. Log3 ("TechemHKV", $dbg, "crc error $msg");
  246. return undef;
  247. } else {
  248. $t = substr $msg, 3, 18;
  249. }
  250. for (my $i = 0; $i<$fb; $i++) {
  251. if ((substr $msg, 57 + ($i * 36), 4) ne TechemHKV_crc16_13757(substr $msg, 25 + ($i * 36), 32)) {
  252. Log3 ("TechemHKV", $dbg, "crc error $msg");
  253. return undef;
  254. } else {
  255. $t .= substr $msg, 25 + ($i * 36), 32;
  256. }
  257. }
  258. if ($rb) {
  259. if ((substr $msg, 25 + ($fb * 36) + ($rb * 2), 4) ne TechemHKV_crc16_13757(substr $msg, 25 + ($fb * 36), $rb * 2)) {
  260. Log3 ("TechemHKV", $dbg, "crc error $msg");
  261. return undef;
  262. } else {
  263. $t .= substr $msg, 25 + ($fb * 36), ($rb * 2);
  264. }
  265. }
  266. Log3 ("TechemHKV", $dbg, "ok $t");
  267. return $t;
  268. }
  269. sub
  270. TechemHKV_ParseActualDate(@) {
  271. my $b = hex($_[0]);
  272. my @t = localtime(time);
  273. my $d = ($b >> 4) & 0x1F;
  274. my $m = ($b >> 9) & 0x0F;
  275. my $y = $t[5] + 1900;
  276. return ($y, $m, $d);
  277. }
  278. sub
  279. TechemHKV_ParseLastDate(@) {
  280. my $b = hex($_[0]);
  281. my $d = ($b >> 0) & 0x1F;
  282. my $m = ($b >> 5) & 0x0F;
  283. my $y = ($b >> 9) & 0x3F;
  284. return ($y, $m, $d);
  285. }
  286. sub
  287. TechemHKV_createCrcTable(@) {
  288. my $poly = 0x3D65;
  289. my $c;
  290. my @table;
  291. for (my $i=0; $i<256; $i++) {
  292. $c = ($i << 8);
  293. for (my $j=0; $j<8; $j++) {
  294. if (($c & 0x8000) != 0) {
  295. $c = 0xFFFF & (($c << 1) ^ $poly);
  296. } else {
  297. $c <<= 1;
  298. }
  299. }
  300. $table[$i] = $c;
  301. }
  302. return \@table;
  303. }
  304. sub
  305. TechemHKV_crc16_13757(@) {
  306. my ($msg) = @_;
  307. my @table = @{$data{WMBUS}{crc_table_13757}};
  308. my @in = split '', pack 'H*', $msg;
  309. my $crc = 0x0000;
  310. for (my $i=0; $i<int(@in); $i++) {
  311. $crc = 0xffff & ( ($crc << 8) ^ $table[(($crc >> 8) ^ ord($in[$i]))] );
  312. }
  313. return sprintf ("%04lX", $crc ^ 0xFFFF);
  314. }
  315. # message bus ahead
  316. # sub
  317. #TechemHKV_subscribe(@) {
  318. # my ($hash, $topic) = @_;
  319. # broker::subscribe ($topic, $hash->{NAME}, \&TechemHKV_rcvBCST);
  320. # return undef;
  321. #}
  322. #sub
  323. #TechemHKV_sendBCST(@) {
  324. # my ($hash, $topic, $msg) = @_;
  325. # broker::publish ($topic, $hash->{NAME}, $msg);
  326. # return undef;
  327. #}
  328. #sub
  329. #TechemHKV_rcvBCST(@) {
  330. # my ($name, $topic, $sender, $msg) = @_;
  331. # my $hash = $defs{$name};
  332. # return undef;
  333. #}
  334. 1;
  335. =pod
  336. =item summary read techem data meter for heating device.
  337. =item summary_DE Anbindung von Techem Heizkostenverteilern.
  338. =begin html
  339. <a name="TechemHKV"></a>
  340. <h3>TechemHKV</h3>
  341. <ul>
  342. This module reads the transmission of techem data meter for heating device.
  343. <br><br>
  344. It will display
  345. <ul>
  346. <li>meter data for current billing period</li>
  347. <li>meter data for previous billing period including date of request</li>
  348. <li>both temperature sensors (if supported by data meter)</li>
  349. </ul>
  350. <br>
  351. It will require a CUL in WMBUS_T mode, although the CUL may temporary set into that mode.
  352. The module keeps track of the CUL rfmode.
  353. <br>
  354. <br>
  355. <a name="TechemHKV_Define"></a>
  356. <b>Define</b>
  357. <br>
  358. <code>define &lt;name&gt; TechemHKV &lt;4|8 digit ID&gt; [&lt;speaking name&gt;]</code>
  359. <ul>
  360. <li>ID: 4 digit ID displayed at techem or 8 digit as printed on bill</li>
  361. <li>speaking name: (optional) human readable identification</li>
  362. </ul>
  363. <br>
  364. <a name="TechemHKV_Readings"></a>
  365. <b>Readings</b>
  366. <ul>
  367. <li>current_period: meter data for current billing period
  368. <br><i>unit-less data, cumulated since start of the current billing period. The reading will be updated once a day, after receiving the first update. Reading time will reflect the time of data (not the time where they were received)</i></br>
  369. </li>
  370. <li>previous_period: meter data for last billing period
  371. <br><i>unit-less data, sum of the last billing period. The reading will be updated only if a new billing period starts. Reading time will reflect the last day of previous billing period (not the time where they were received)</i></br>
  372. </li>
  373. <li>temp1: ambient temperature</li>
  374. <li>temp2: heater surface temperature</li>
  375. <br>
  376. </ul>
  377. <a name="TechemHKV_Internals"></a>
  378. <b>Internals</b>
  379. <ul>
  380. <li>friendly: human readable identification of meter as specified by define</li>
  381. <li>longID: 8 digit id of meter</li>
  382. <br>
  383. </ul>
  384. </ul>
  385. =end html
  386. =begin html_DE
  387. <a name="TechemHKV"></a>
  388. <h3>TechemHKV</h3>
  389. <ul>
  390. Das modul empfängt Daten eines Techem Heizkostenverteilers.
  391. <br><br>
  392. Empfangen werden
  393. <ul>
  394. <li>Wert des aktuellen Abrechnungszeitraumes</li>
  395. <li>Wert des vorhergehenden Abrechnungszeitraumes einschließlich des Ablesedatums</li>
  396. <li>Beide Temperatur Sensoren (sofern der Heizkostenverteiler sie sendet)</li>
  397. </ul>
  398. <br>
  399. Zum Empfang wird ein CUL im WMBUS_T mode benötigt. Dabei ist es ausreichend ihn vorrübergehend in diesen Modus zu schalten.
  400. Das Modul überwacht den rfmode aller verfügbaren CUL
  401. <br>
  402. <br>
  403. <a name="TechemHKV_Define"></a>
  404. <b>Define</b>
  405. <br>
  406. <code>define &lt;name&gt; TechemHKV &lt;4|8 digit ID&gt; [&lt;speaking name&gt;]</code>
  407. <ul>
  408. <li>ID: 4 Ziffern wie auf dem Heizkostenverteiler angezeigt oder 8 Ziffern aus der Abrechnung</li>
  409. <li>speaking name: (optional) Bezeichnung</li>
  410. </ul>
  411. <br>
  412. <a name="TechemHKV_Readings"></a>
  413. <b>Readings</b>
  414. <ul>
  415. <li>current_period: Wert des aktuellen Abrechnungszeitraumes
  416. <br><i>Der kumulierte (einheitenlose) Verbrauch seid dem Start des aktuellen Abrechnungszeitraumes. Das reading wird einmal am Tag aktualisiert. Die Zeit kennzeichnet den Stand der Daten. (und nicht den Empfangszeitpunkt der Daten)</i></br>
  417. </li>
  418. <li>previous_period: Summe des letzten Abrechnungszeitraum
  419. <br><i>Die (einheitenlose) Summe der Verbauchs im gesamten letzten Abrechnungszeitraum. Das reading wird jeweils zu Beginn eines neuen Abrechnungszeitraumes aktualisiert. Die Zeit kennzeichnet das Ablesedatum also das Ende des vorherigen Abrechnugszeitraumes. (und nicht den Empfangszeitpunkt der Daten)</i></br>
  420. </li>
  421. <li>temp1: Umgebungstemperatur</li>
  422. <li>temp2: Oberflächentemperatur des Heizkörpers</li>
  423. <br>
  424. </ul>
  425. <a name="TechemHKV_Internals"></a>
  426. <b>Internals</b>
  427. <ul>
  428. <li>friendly: die beim define übergebene, zusätzliche Bezeichnung</li>
  429. <li>longID: 8 Ziffern ID des Heizkostenverteilers</li>
  430. <br>
  431. </ul>
  432. </ul>
  433. =end html_DE
  434. =cut