############################################################################### # $Id$ # # this module is part of fhem under the same license # copyright 2015, joerg herrmann # # history # initial checkin # ############################################################################### package main; use strict; use warnings; use Time::HiRes qw(time); my %typeText = ( '62' => 'warm water', # '72' => 'cold water', # '43' => 'heat meter', # compact V ); sub TechemWZ_Initialize(@) { my ($hash) = @_; # require "Broker.pm"; $hash->{Match} = "^b..446850[\\d]{8}..(?:43|45|62|72).*"; $hash->{DefFn} = "TechemWZ_Define"; $hash->{UndefFn} = "TechemWZ_Undef"; $hash->{SetFn} = "TechemWZ_Set"; $hash->{GetFn} = "TechemWZ_Get"; $hash->{NotifyFn} = "TechemWZ_Notify"; $hash->{ParseFn} = "TechemWZ_Parse"; $hash->{AttrList} = "".$readingFnAttributes; return undef; } sub TechemWZ_Define(@) { my ($hash, $def) = @_; my ($name, $t, $id); ($name, $t, $id, $def) = split(/ /, $def,4); return "ID must have 8 digits" if ($id !~ /^\d{8}$/); return "ID $id already defined" if exists($modules{TechemWZ}{defptr}{$id}); # house keeping if (exists($hash->{OLDDEF}) && ($hash->{DEF} ne $hash->{OLDDEF}) ) { my @a = split(/ /, $hash->{OLDDEF}); delete($hash->{VERSION}); delete($hash->{METER}); delete($hash->{READINGS}); delete($modules{TechemWZ}{defptr}{$a[0]}); delete($hash->{helper}->{list}); } # create crc table if required $data{WMBUS}{crc_table_13757} = TechemWZ_createCrcTable() unless (exists($data{WMBUS}{crc_table_13757})); $hash->{helper}->{listmode} = ($id eq '00000000')?1:0; $hash->{ID} = $id; $modules{TechemWZ}{defptr}{$id} = $hash; $hash->{FRIENDLY} = $def if (defined($def)); # subscribe broadcast channels # TechemWZ_subscribe($hash, 'foo'); TechemWZ_Run($hash) if $init_done; return undef; } sub TechemWZ_Undef(@) { my ($hash) = @_; my $id = $hash->{ID}; delete($modules{TechemWZ}{defptr}{$id}); return undef; } sub TechemWZ_Set(@) { my ($hash, $name, $cmd, @args) = @_; my $cnt = @args; return undef; } sub TechemWZ_Get(@) { my ($hash, $name, $cmd, @args) = @_; return undef unless ($hash->{helper}->{listmode}); return "unknown command ($cmd): choose one of list" if ($cmd eq "?"); return "unknown command ($cmd): choose one of list" if ($cmd ne "list"); my $result = ""; my $l = $hash->{helper}->{list}; foreach my $key (sort { $l->{$a}->{msg}->{meter} <=> $l->{$b}->{msg}->{meter} } keys %{$l} ) { $result .= "$l->{$key}->{msg}->{long}\t"; $result .= $typeText{$l->{$key}->{msg}->{type}}."\t"; $result .= "$l->{$key}->{msg}->{meter}\t"; $result .= "$l->{$key}->{msg}->{rssi}\t\n"; } return $result; } sub TechemWZ_Notify (@) { my ($hash, $ntfyDev) = @_; return unless (($ntfyDev->{TYPE} eq 'CUL') || ($ntfyDev->{TYPE} eq 'Global')); foreach my $event (@{$ntfyDev->{CHANGED}}) { my @e = split(' ', $event); next unless defined($e[0]); TechemWZ_Run($hash) if ($e[0] eq 'INITIALIZED'); # patch CUL.pm TechemWZ_IOPatch($hash, $e[1]) if (($e[0] eq 'ATTR') && ($e[2] eq 'rfmode') && ($e[3] eq 'WMBus_T')); # disable receiver if (($e[0] eq 'ATTR') && ($e[2] eq 'rfmode') && ($e[3] ne 'WMBus_T')) { readingsBeginUpdate($hash); readingsBulkUpdate($hash, "state", "standby (IO missing)", 1); readingsEndUpdate($hash, 1); } } return undef; } sub TechemWZ_Receive(@) { my ($hash, $msg, $raw) = @_; my @t = localtime(time); my ($ats, $ts); $hash->{VERSION} = $msg->{version}; $hash->{METER} = $typeText{$msg->{type}}; delete $hash->{CHANGETIME}; # clean up, workaround for fhem prior http://forum.fhem.de/index.php/topic,47474.msg391964.html#msg391964 # day period changed $ats = ReadingsTimestamp($hash->{NAME},"current_period", "0"); $ts = sprintf ("%02d-%02d-%02d 00:00:00", $msg->{actual}->{year}, $msg->{actual}->{month}, $msg->{actual}->{day}); if ($ats ne $ts) { my $i; readingsBeginUpdate($hash); $hash->{".updateTimestamp"} = $ts; $i = $#{ $hash->{CHANGED} }; readingsBulkUpdate($hash, "meter", $msg->{meter}); $hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts if ($#{ $hash->{CHANGED} } != $i ); # only add ts if there is a event to $i = $#{ $hash->{CHANGED} }; readingsBulkUpdate($hash, "current_period", $msg->{actualVal}); $hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts if ($#{ $hash->{CHANGED} } != $i ); # only add ts if there is a event to readingsEndUpdate($hash, 1); } # billing period changed $ats = ReadingsTimestamp($hash->{NAME},"previous_period", "0"); $ts = sprintf ("20%02d-%02d-%02d 00:00:00", $msg->{last}->{year}, $msg->{last}->{month}, $msg->{last}->{day}); if ($ats ne $ts) { my $i; readingsBeginUpdate($hash); $hash->{".updateTimestamp"} = $ts; $i = $#{ $hash->{CHANGED} }; readingsBulkUpdate($hash, "previous_period", $msg->{lastVal}); $hash->{CHANGETIME}->[$#{ $hash->{CHANGED} }] = $ts if ($#{ $hash->{CHANGED} } != $i ); # only add ts if there is a event to readingsEndUpdate($hash, 1); } return undef; } sub TechemWZ_Run(@) { my ($hash) = @_; # find a CUL foreach my $d (keys %defs) { # live patch CUL.pm TechemWZ_IOPatch($hash, $d) if ($defs{$d}{TYPE} eq "CUL"); } return undef; } # live patch CUL.pm, aka THE HACK sub TechemWZ_IOPatch(@) { my ($hash, $iodev) = @_; return undef unless (AttrVal($iodev, 'rfmode', '') eq 'WMBus_T'); # see if already patched readingsSingleUpdate($hash, 'state', 'listening', 1); return undef if ($defs{$iodev}{Clients} =~ /TechemWZ/ ); $defs{$iodev}{Clients} = ':TechemWZ'.$defs{$iodev}{Clients}; $defs{$iodev}{'.clientArray'} = undef; return undef; } sub TechemWZ_Parse(@) { my ($iohash, $msg) = @_; my ($message, $rssi); ($msg, $rssi) = split (/::/, $msg); $msg = TechemWZ_SanityCheck($msg); return '' unless $msg; my @m = ($msg =~ m/../g); my @d; # parse ($message->{long}, $message->{short}) = TechemWZ_ParseID(@m); $message->{type} = TechemWZ_ParseSubType(@m); $message->{version} = TechemWZ_ParseSubVersion(@m); $message->{rssi} = ($rssi)?$rssi:"?"; # metertype specific adjustment if ($message->{type} =~ /62|72/) { $message->{lastVal} = TechemWZ_ParseLastPeriod(@m); $message->{actualVal} = TechemWZ_ParseActualPeriod(@m); ($message->{actual}->{year}, $message->{actual}->{month}, $message->{actual}->{day}) = TechemWZ_ParseActualDate(@m); ($message->{last}->{year}, $message->{last}->{month}, $message->{last}->{day}) = TechemWZ_ParseLastDate(@m); $message->{lastVal} /= 10; $message->{actualVal} /= 10; $message->{meter} = $message->{lastVal} + $message->{actualVal}; } elsif ($message->{type} =~ /43|45/) { $message->{lastVal} = TechemWZ_WMZ_Type1_ParseLastPeriod(@m); $message->{actualVal} = TechemWZ_WMZ_Type1_ParseActualPeriod(@m); ($message->{actual}->{year}, $message->{actual}->{month}, $message->{actual}->{day}) = TechemWZ_WMZ_Type1_ParseActualDate(@m); ($message->{last}->{year}, $message->{last}->{month}, $message->{last}->{day}) = TechemWZ_ParseLastDate(@m); $message->{meter} = $message->{lastVal} + $message->{actualVal}; } # list if (exists( $modules{TechemWZ}{defptr}{'00000000'} ) && defined( $defs{$modules{TechemWZ}{defptr}{'00000000'}->{NAME}} )) { my $listdev = $modules{TechemWZ}{defptr}{'00000000'}; $listdev->{helper}->{list}->{$message->{long}}->{msg} = $message; push @d, $listdev->{NAME}; } # dispatch if (exists( $modules{TechemWZ}{defptr}{$message->{long}})) { my $deviceHash = $modules{TechemWZ}{defptr}{$message->{long}}; TechemWZ_Receive($deviceHash, $message); push @d, $deviceHash->{NAME}; } if (defined($d[0])) { return (@d); } else { return (''); # discard neighbor devices } } sub TechemWZ_SanityCheck(@) { my ($msg) = @_; my $rssi; my $t; my $dbg = 4; #($msg, $rssi) = split (/::/, $msg); my @m = ((substr $msg,1) =~ m/../g); # at least 3 chars if (length($msg) < 3) { Log3 ("TechemWZ", $dbg, "msg incomplete $msg"); return undef; } # msg length without crc blocks my $l = hex(substr $msg, 1, 2) + 1; # full crc payload blocks my $fb = int(($l - 10) / 16); # remaining bytes ? my $rb = ($l - 10) % 16; # required len my $rl = $l + 2 + ($fb * 2) + (($rb)?2:0); if (($rl * 2) > (length($msg) -1)) { Log3 ("TechemWZ", $dbg, "msg incomplete $msg"); return undef; } # CRC first 10 byte, then chunks of 16 byte then remaining if ((substr $msg, 21, 4) ne TechemWZ_crc16_13757(substr $msg, 1, 20)) { Log3 ("TechemWZ", $dbg, "crc error $msg"); return undef; } else { $t = substr $msg, 3, 18; } for (my $i = 0; $i<$fb; $i++) { if ((substr $msg, 57 + ($i * 36), 4) ne TechemWZ_crc16_13757(substr $msg, 25 + ($i * 36), 32)) { Log3 ("TechemWZ", $dbg, "crc error $msg"); return undef; } else { $t .= substr $msg, 25 + ($i * 36), 32; } } if ($rb) { if ((substr $msg, 25 + ($fb * 36) + ($rb * 2), 4) ne TechemWZ_crc16_13757(substr $msg, 25 + ($fb * 36), $rb * 2)) { Log3 ("TechemWZ", $dbg, "crc error $msg"); return undef; } else { $t .= substr $msg, 25 + ($fb * 36), ($rb * 2); } } return $t; } sub TechemWZ_ParseID(@) { my @m = @_; return ("$m[6]$m[5]$m[4]$m[3]", "$m[4]$m[3]"); } sub TechemWZ_ParseSubType(@) { my @m = @_; return "$m[8]"; } sub TechemWZ_ParseSubVersion(@) { my @m = @_; return "$m[7]"; } sub TechemWZ_ParseLastPeriod(@) { my @m = @_; return hex("$m[14]$m[13]"); } sub TechemWZ_ParseActualPeriod(@) { my @m = @_; return hex("$m[18]$m[17]"); } sub TechemWZ_ParseActualDate(@) { my @m = @_; my @t = localtime(time); my $b = hex("$m[16]$m[15]"); my $d = ($b >> 4) & 0x1F; my $m = ($b >> 9) & 0x0F; my $y = $t[5] + 1900; return ($y, $m, $d); } sub TechemWZ_ParseLastDate(@) { my @m = @_; my $b = hex("$m[12]$m[11]"); my $d = ($b >> 0) & 0x1F; my $m = ($b >> 5) & 0x0F; my $y = ($b >> 9) & 0x3F; return ($y, $m, $d); } ############################################################################### # # Compact 5 heatmeter # ############################################################################### sub TechemWZ_WMZ_Type1_ParseLastPeriod(@) { my @m = @_; return hex("$m[15]$m[14]$m[13]"); } sub TechemWZ_WMZ_Type1_ParseActualPeriod(@) { my @m = @_; return hex("$m[19]$m[18]$m[17]"); } sub TechemWZ_WMZ_Type1_ParseActualDate(@) { my @m = @_; my @t = localtime(time); my $b = hex("$m[21]$m[20]"); my $d = ($b >> 7) & 0x1F; my $m = (hex("$m[16]") >> 3) & 0x0F; my $y = $t[5] + 1900; return ($y, $m, $d); } sub TechemWZ_createCrcTable(@) { my $poly = 0x3D65; my $c; my @table; for (my $i=0; $i<256; $i++) { $c = ($i << 8); for (my $j=0; $j<8; $j++) { if (($c & 0x8000) != 0) { $c = 0xFFFF & (($c << 1) ^ $poly); } else { $c <<= 1; } } $table[$i] = $c; } return \@table; } sub TechemWZ_crc16_13757(@) { my ($msg) = @_; my @table = @{$data{WMBUS}{crc_table_13757}}; my @in = split '', pack 'H*', $msg; my $crc = 0x0000; for (my $i=0; $i> 8) ^ ord($in[$i]))] ); } return sprintf ("%04lX", $crc ^ 0xFFFF); } # message bus ahead # sub #TechemWZ_subscribe(@) { # my ($hash, $topic) = @_; # broker::subscribe ($topic, $hash->{NAME}, \&TechemWZ_rcvBCST); # return undef; #} #sub #TechemWZ_sendBCST(@) { # my ($hash, $topic, $msg) = @_; # broker::publish ($topic, $hash->{NAME}, $msg); # return undef; #} #sub #TechemWZ_rcvBCST(@) { # my ($name, $topic, $sender, $msg) = @_; # my $hash = $defs{$name}; # return undef; #} 1; =pod =item summary This module reads the transmission of techem volume data meter. =item summary_DE Das modul empfängt Daten von Techem Volumenzählern. =begin html

TechemWZ

=end html =begin html_DE

TechemWZ

=end html_DE =cut