mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-03-03 16:56:54 +00:00
New module 36_WMBUS.pm for Wireless M-Bus with support module WMBus.pm
git-svn-id: https://svn.fhem.de/fhem/trunk@6389 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
parent
dd4da9d6ea
commit
dcc214d15e
@ -1,5 +1,6 @@
|
||||
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
|
||||
# Do not insert empty lines here, update check depends on it.
|
||||
- added: new module 36_WMBUS.pm (kaihs) Wireless M-Bus
|
||||
- feature: SYSMON: aded new plots (power infos for cubietruck)
|
||||
- feature: SYSMON: aded new readings for each network interface: ip and ip6
|
||||
- feature: SYSMON: aded power supply informations to the text output method
|
||||
|
391
fhem/FHEM/36_WMBUS.pm
Normal file
391
fhem/FHEM/36_WMBUS.pm
Normal file
@ -0,0 +1,391 @@
|
||||
#
|
||||
# kaihs@FHEM_Forum (forum.fhem.de)
|
||||
#
|
||||
# $Id: $
|
||||
#
|
||||
#
|
||||
|
||||
package main;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use SetExtensions;
|
||||
use WMBus;
|
||||
|
||||
sub WMBUS_Parse($$);
|
||||
sub WMBUS_SetReadings($$$);
|
||||
|
||||
sub WMBUS_Initialize($) {
|
||||
my ($hash) = @_;
|
||||
|
||||
$hash->{Match} = "^b.*";
|
||||
#$hash->{SetFn} = "WMBUS_Set";
|
||||
#$hash->{GetFn} = "WMBUS_Get";
|
||||
$hash->{DefFn} = "WMBUS_Define";
|
||||
$hash->{UndefFn} = "WMBUS_Undef";
|
||||
#$hash->{FingerprintFn} = "WMBUS_Fingerprint";
|
||||
$hash->{ParseFn} = "WMBUS_Parse";
|
||||
$hash->{AttrFn} = "WMBUS_Attr";
|
||||
$hash->{AttrList} = "IODev".
|
||||
" AESkey".
|
||||
" $readingFnAttributes";
|
||||
}
|
||||
|
||||
sub
|
||||
WMBUS_Define($$)
|
||||
{
|
||||
my ($hash, $def) = @_;
|
||||
my @a = split("[ \t][ \t]*", $def);
|
||||
my $mb;
|
||||
|
||||
if(@a != 6 && @a != 3) {
|
||||
my $msg = "wrong syntax: define <name> WMBUS [<ManufacturerID> <SerialNo> <Version> <Type>]|b<HexMessage>";
|
||||
Log3 undef, 2, $msg;
|
||||
return $msg;
|
||||
}
|
||||
|
||||
my $name = $a[0];
|
||||
|
||||
if (@a == 3) {
|
||||
# unparsed message
|
||||
my $msg = $a[2];
|
||||
|
||||
return "a WMBus message must be a least 12 bytes long" if $msg !~ m/b[a-zA-Z0-9]{24,}/;
|
||||
|
||||
$mb = new WMBus;
|
||||
if ($mb->parseLinkLayer(pack('H*',substr($msg,1)))) {
|
||||
$hash->{Manufacturer} = $mb->{manufacturer};
|
||||
$hash->{IdentNumber} = $mb->{afield_id};
|
||||
$hash->{Version} = $mb->{afield_ver};
|
||||
$hash->{DeviceType} = $mb->{afield_type};
|
||||
if ($mb->{errormsg}) {
|
||||
$hash->{Error} = $mb->{errormsg};
|
||||
} else {
|
||||
delete $hash->{Error};
|
||||
}
|
||||
} else {
|
||||
return "failed to parse msg: $mb->{errormsg}";
|
||||
}
|
||||
|
||||
} else {
|
||||
# manual specification
|
||||
# $a[2] =~ m/[A-Z]{3}/;
|
||||
# return "$a[2] is not a valid WMBUS manufacturer id" if( !defined($1) );
|
||||
|
||||
# $a[3] =~ m/[0-9]{1,8}/;
|
||||
# return "$a[3] is not a valid WMBUS serial number id" if( !defined($1) );
|
||||
|
||||
# $a[4] =~ m/[0-9]{1,2}/;
|
||||
# return "$a[4] is not a valid WMBUS version" if( !defined($1) );
|
||||
|
||||
# $a[5] =~ m/[0-9]{1,2}/;
|
||||
# return "$a[5] is not a valid WMBUS type" if( !defined($1) );
|
||||
|
||||
$hash->{Manufacturer} = $a[2];
|
||||
$hash->{IdentNumber} = $a[3];
|
||||
$hash->{Version} = $a[4];
|
||||
$hash->{DeviceType} = $a[5];
|
||||
|
||||
}
|
||||
my $addr = join("_", $hash->{Manufacturer},$hash->{IdentNumber},$hash->{Version},$hash->{DeviceType}) ;
|
||||
|
||||
return "WMBUS device $addr already used for $modules{WMBUS}{defptr}{$addr}->{NAME}." if( $modules{WMBUS}{defptr}{$addr}
|
||||
&& $modules{WMBUS}{defptr}{$addr}->{NAME} ne $name );
|
||||
|
||||
$modules{WMBUS}{defptr}{$addr} = $hash;
|
||||
|
||||
AssignIoPort($hash);
|
||||
if(defined($hash->{IODev}->{NAME})) {
|
||||
Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME};
|
||||
} else {
|
||||
Log3 $name, 1, "$name: no I/O device";
|
||||
}
|
||||
|
||||
$hash->{DEF} = join(" ", $hash->{Manufacturer},$hash->{IdentNumber},$hash->{Version},$hash->{DeviceType});
|
||||
|
||||
$hash->{DeviceMedium} = WMBus::->type2string($hash->{DeviceType});
|
||||
if (defined($mb)) {
|
||||
|
||||
if ($mb->parseApplicationLayer()) {
|
||||
if ($mb->{cifield} == WMBus::CI_RESP_12) {
|
||||
$hash->{Meter_Id} = $mb->{meter_id};
|
||||
$hash->{Meter_Manufacturer} = $mb->manId2ascii($mb->{meter_man});
|
||||
$hash->{Meter_Version} = $mb->{meter_vers};
|
||||
$hash->{Meter_Dev} = $mb->{meter_devtypestring};
|
||||
$hash->{Access_No} = $mb->{access_no};
|
||||
$hash->{Status} = $mb->{status};
|
||||
}
|
||||
WMBUS_SetReadings($hash, $name, $mb);
|
||||
} else {
|
||||
$hash->{Error} = $mb->{errormsg};
|
||||
}
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
#####################################
|
||||
sub
|
||||
WMBUS_Undef($$)
|
||||
{
|
||||
my ($hash, $arg) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
my $addr = $hash->{addr};
|
||||
|
||||
#delete( $modules{WMBUS}{defptr}{$addr} );
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
#####################################
|
||||
sub
|
||||
WMBUS_Get($@)
|
||||
{
|
||||
my ($hash, $name, $cmd, @args) = @_;
|
||||
|
||||
return "\"get $name\" needs at least one parameter" if(@_ < 3);
|
||||
|
||||
my $list = "";
|
||||
|
||||
return "Unknown argument $cmd, choose one of $list";
|
||||
}
|
||||
|
||||
sub
|
||||
WMBUS_Fingerprint($$)
|
||||
{
|
||||
my ($name, $msg) = @_;
|
||||
|
||||
return ( "", $msg );
|
||||
}
|
||||
|
||||
|
||||
sub
|
||||
WMBUS_Parse($$)
|
||||
{
|
||||
my ($hash, $msg) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
my $addr;
|
||||
my $rhash;
|
||||
|
||||
# $hash is the hash of the IODev!
|
||||
|
||||
if( $msg =~ m/^b/ ) {
|
||||
# WMBus message received
|
||||
|
||||
Log3 $name, 5, "WMBUS raw msg " . $msg;
|
||||
|
||||
my $mb = new WMBus;
|
||||
|
||||
if ($mb->parseLinkLayer(pack('H*',substr($msg,1)))) {
|
||||
$addr = join("_", $mb->{manufacturer}, $mb->{afield_id}, $mb->{afield_ver}, $mb->{afield_type});
|
||||
|
||||
$rhash = $modules{WMBUS}{defptr}{$addr};
|
||||
|
||||
if( !$rhash ) {
|
||||
Log3 $name, 3, "WMBUS Unknown device $msg, please define it";
|
||||
|
||||
return "UNDEFINED WMBUS_$addr WMBUS $msg";
|
||||
}
|
||||
my $rname = $rhash->{NAME};
|
||||
my $aeskey;
|
||||
|
||||
|
||||
if ($aeskey = AttrVal($rname, 'AESkey', undef)) {
|
||||
$mb->{aeskey} = pack("H*",$aeskey);
|
||||
} else {
|
||||
$mb->{aeskey} = undef;
|
||||
}
|
||||
if ($mb->parseApplicationLayer()) {
|
||||
return WMBUS_SetReadings($rhash, $rname, $mb);
|
||||
} else {
|
||||
Log3 $rname, 2, "WMBUS $rname Error during ApplicationLayer parse:" . $mb->{errormsg};
|
||||
readingsSingleUpdate($rhash, "state", $mb->{errormsg}, 1);
|
||||
return $rname;
|
||||
}
|
||||
} else {
|
||||
# error
|
||||
Log3 $name, 2, "WMBUS Error during LinkLayer parse:" . $mb->{errormsg};
|
||||
return undef;
|
||||
}
|
||||
} else {
|
||||
DoTrigger($name, "UNKNOWNCODE $msg");
|
||||
Log3 $name, 3, "$name: Unknown code $msg, help me!";
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
|
||||
sub WMBUS_SetReadings($$$)
|
||||
{
|
||||
my $hash = shift;
|
||||
my $name = shift;
|
||||
my $mb = shift;
|
||||
|
||||
my @list;
|
||||
push(@list, $name);
|
||||
|
||||
readingsBeginUpdate($hash);
|
||||
|
||||
if ($mb->{decrypted}) {
|
||||
my $dataBlocks = $mb->{datablocks};
|
||||
my $dataBlock;
|
||||
|
||||
for $dataBlock ( @$dataBlocks ) {
|
||||
if ( $dataBlock->{type} eq "MANUFACTURER SPECIFIC") {
|
||||
readingsBulkUpdate($hash, "$dataBlock->{number}:type", $dataBlock->{type});
|
||||
} else {
|
||||
readingsBulkUpdate($hash, "$dataBlock->{number}:storage_no", $dataBlock->{storageNo});
|
||||
readingsBulkUpdate($hash, "$dataBlock->{number}:type", $dataBlock->{type});
|
||||
readingsBulkUpdate($hash, "$dataBlock->{number}:value", $dataBlock->{value});
|
||||
readingsBulkUpdate($hash, "$dataBlock->{number}:unit", $dataBlock->{unit});
|
||||
}
|
||||
}
|
||||
}
|
||||
readingsBulkUpdate($hash, "is_encrypted", $mb->{isEncrypted});
|
||||
readingsBulkUpdate($hash, "decryption_ok", $mb->{decrypted});
|
||||
if ($mb->{decrypted}) {
|
||||
readingsBulkUpdate($hash, "state", $mb->{statusstring});
|
||||
} else {
|
||||
readingsBulkUpdate($hash, "state", 'decryption failed');
|
||||
}
|
||||
|
||||
readingsEndUpdate($hash,1);
|
||||
|
||||
return @list;
|
||||
|
||||
}
|
||||
|
||||
#####################################
|
||||
sub
|
||||
WMBUS_Set($@)
|
||||
{
|
||||
my ($hash, @a) = @_;
|
||||
|
||||
my $name = shift @a;
|
||||
my $cmd = shift @a;
|
||||
my $arg = join(" ", @a);
|
||||
|
||||
|
||||
my $list = "resetAccumulatedPower";
|
||||
return $list if( $cmd eq '?' || $cmd eq '');
|
||||
|
||||
|
||||
if($cmd eq "resetAccumulatedPower") {
|
||||
CommandAttr(undef, "$name accumulatedPowerOffset " . $hash->{READINGS}{accumulatedPowerMeasured}{VAL});
|
||||
}
|
||||
else {
|
||||
return "Unknown argument $cmd, choose one of ".$list;
|
||||
}
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub
|
||||
WMBUS_Attr(@)
|
||||
{
|
||||
my ($cmd, $name, $attrName, $attrVal) = @_;
|
||||
my $hash = $defs{$name};
|
||||
my $msg = '';
|
||||
|
||||
if ($attrName eq 'AESkey') {
|
||||
if ($attrVal =~ /^[0-9A-Fa-f]{32}$/) {
|
||||
$hash->{wmbus}->{aeskey} = $attrVal;
|
||||
} else {
|
||||
$msg = "AESkey must be a 32 digit hexadecimal value";
|
||||
}
|
||||
|
||||
}
|
||||
return ($msg) ? $msg : undef;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=pod
|
||||
=begin html
|
||||
|
||||
<a name="WMBUS"></a>
|
||||
<h3>WMBUS - Wireless M-Bus</h3>
|
||||
<ul>
|
||||
This module supports Wireless M-Bus meters for e.g. water, heat, gas or electricity.
|
||||
Wireless M-Bus is a standard protocol supported by various manufaturers.
|
||||
|
||||
It uses the 868 MHz band for radio transmissions.
|
||||
Therefore you need a device which can receive Wireless M-Bus messages, e.g. a <a href="#CUL">CUL</a> with culfw >= 1.59.
|
||||
<br>
|
||||
WMBus uses two different radio protocols, T-Mode and S-Mode. The receiver must be configured to use the same protocol as the sender.
|
||||
In case of a CUL this can be done by setting <a href="#rfmode">rfmode</a> to WMBus_T or WMBus_S respectively.
|
||||
<br>
|
||||
WMBus devices send data periodically depending on their configuration. It can take days between individual messages or they might be sent
|
||||
every minute.
|
||||
<br>
|
||||
WMBus messages can be optionally encrypted. In that case the matching AESkey must be specified with attr AESkey. Otherwise the decryption
|
||||
will fail and no relevant data will be available.
|
||||
<br><br>
|
||||
<a name="WMBUSdefine"></a>
|
||||
<b>Define</b>
|
||||
<ul>
|
||||
<code>define <name> WMBUS [<manufacturer id> <identification number> <version> <type>]|<bHexCode></code> <br>
|
||||
<br>
|
||||
Normally a WMBus device isn't defined manually but automatically through the autocreate mechanism upon the first reception of a message.
|
||||
<br>
|
||||
For manual definition there are two ways.
|
||||
<ul>
|
||||
<li>
|
||||
Specifying a raw WMBus message as received by a CUL. Such a message starts with a lower case 'b' and contains at least 24 hexadecimal digits.
|
||||
The WMBUS module extracts all relevant information from such a message.
|
||||
</li>
|
||||
<li>
|
||||
Explictly specifify the information that uniquely identifies a WMBus device. <br>
|
||||
The manufacturer code, which is is a three letter shortcut of the manufacturer name. See
|
||||
<a href="http://dlms.com/organization/flagmanufacturesids/index.html">dlms.com</a> for a list of registered ids.<br>
|
||||
The identification number is the serial no of the meter.<br>
|
||||
version is the version code of the meter<br>
|
||||
type is the type of the meter, e.g. water or electricity encoded as a number.
|
||||
</li>
|
||||
<br>
|
||||
</ul>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
<a name="WMBUSset"></a>
|
||||
<b>Set</b> <ul>N/A</ul><br>
|
||||
<a name="WMBUSget"></a>
|
||||
<b>Get</b> <ul>N/A</ul><br>
|
||||
|
||||
<a name="WMBUSattr"></a>
|
||||
<b>Attributes</b>
|
||||
<ul>
|
||||
<li><a href="#IODev">IODev</a><br>
|
||||
Set the IO or physical device which should be used for receiving signals
|
||||
for this "logical" device. An example for the physical device is a CUL.
|
||||
</li><br>
|
||||
<li>AESKey<br>
|
||||
A 16 byte AES-Key in hexadecimal digits. Used to decrypt message from meters which have encryption enabled.
|
||||
</li>
|
||||
</ul>
|
||||
<br>
|
||||
<a name="WMBUSreadings"></a>
|
||||
<b>Readings</b><br>
|
||||
<ul>
|
||||
Meters can send a lot of different information depending on their type. An electricity meter will send other data than a water meter.
|
||||
The information also depends on the manufacturer of the meter. See the WMBus specification on <a href="www.oms-group.org">oms-group.org</a> for details.
|
||||
<br><br>
|
||||
The readings are generated in blocks starting with block 1. A meter can send several data blocks.
|
||||
Each block has at least a type, a value and a unit, e.g. for an electricity meter it might look like<br>
|
||||
<ul>
|
||||
<code>1:type VIF_ELECTRIC_ENERGY</code><br>
|
||||
<code>1:unit Wh</code><br>
|
||||
<code>1:value 2948787</code><br>
|
||||
</ul>
|
||||
<br>
|
||||
There is also a fixed set of readings.
|
||||
<ul>
|
||||
<li><code>is_encrypted</code> is 1 if the received message is encrypted.</li>
|
||||
<li><code>decryption_ok</code> is 1 of a message has either been successfully decrypted or if it is unencrypted.</li>
|
||||
<li><code>state</code> contains the state of the meter and may contain error message like battery low. Normally it contains 'no error'.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
=end html
|
||||
=cut
|
955
fhem/FHEM/WMBus.pm
Normal file
955
fhem/FHEM/WMBus.pm
Normal file
@ -0,0 +1,955 @@
|
||||
package WMBus;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use feature qw(say);
|
||||
use Crypt::CBC;
|
||||
use Digest::CRC;
|
||||
|
||||
require Exporter;
|
||||
my @ISA = qw(Exporter);
|
||||
my @EXPORT = qw(new parse parseLinkLayer parseApplicationLayer manId2ascii type2string);
|
||||
|
||||
|
||||
use constant {
|
||||
# sent by meter
|
||||
SND_NR => 0x44, # Send, no reply
|
||||
SND_IR => 0x46, # Send installation request, must reply with CNF_IR
|
||||
ACC_NR => 0x47,
|
||||
ACC_DMD => 0x48,
|
||||
|
||||
# sent by controller
|
||||
SND_NKE => 0x40, # Link reset
|
||||
CNF_IR => 0x06,
|
||||
|
||||
# CI field
|
||||
CI_RESP_4 => 0x7a, # Response from device, 4 Bytes
|
||||
CI_RESP_12 => 0x72, # Response from device, 12 Bytes
|
||||
|
||||
# DIF types (Data Information Field), see page 32
|
||||
DIF_NONE => 0x00,
|
||||
DIF_INT8 => 0x01,
|
||||
DIF_INT16 => 0x02,
|
||||
DIF_INT32 => 0x04,
|
||||
DIF_FLOAT32 => 0x05,
|
||||
DIF_INT48 => 0x06,
|
||||
DIF_INT64 => 0x07,
|
||||
DIF_READOUT => 0x08,
|
||||
DIF_BCD2 => 0x09,
|
||||
DIF_BCD4 => 0x0a,
|
||||
DIF_BCD6 => 0x0b,
|
||||
DIF_BCD8 => 0x0c,
|
||||
DIF_VARLEN => 0x0d,
|
||||
DIF_BCD12 => 0x0e,
|
||||
DIF_SPECIAL => 0x0f,
|
||||
|
||||
|
||||
DIF_IDLE_FILLER => 0x2f,
|
||||
|
||||
DIF_EXTENSION_BIT => 0x80,
|
||||
|
||||
VIF_EXTENSION => 0xFB, # true VIF is given in the first VIFE and is coded using table 8.4.4 b) (128 new VIF-Codes)
|
||||
VIF_EXTENSION_BIT => 0x80,
|
||||
|
||||
|
||||
ERR_NO_ERROR => 0,
|
||||
ERR_CRC_FAILED => 1,
|
||||
ERR_UNKNOWN_VIFE => 2,
|
||||
ERR_UNKNOWN_VIF => 3,
|
||||
ERR_TOO_MANY_DIFE => 4,
|
||||
ERR_UNKNOWN_LVAR => 5,
|
||||
ERR_UNKNOWN_DATAFIELD => 6,
|
||||
ERR_UNKNOWN_CIFIELD => 7,
|
||||
ERR_DECRYPTION_FAILED => 8,
|
||||
ERR_NO_AESKEY => 9,
|
||||
ERR_UNKNOWN_ENCRYPTION => 10,
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
sub valueCalcNumeric($$) {
|
||||
my $value = shift;
|
||||
my $dataBlock = shift;
|
||||
|
||||
return $value * $dataBlock->{valueFactor};
|
||||
}
|
||||
|
||||
sub valueCalcDate($$) {
|
||||
my $value = shift;
|
||||
my $dataBlock = shift;
|
||||
|
||||
#value is a 16bit int
|
||||
|
||||
#day: UI5 [1 to 5] <1 to 31>
|
||||
#month: UI4 [9 to 12] <1 to 12>
|
||||
#year: UI7[6 to 8,13 to 16] <0 to 99>
|
||||
|
||||
# YYYY MMMM YYY DDDDD
|
||||
# 0b0000 1100 111 11111 = 31.12.2007
|
||||
# 0b0000 0100 111 11110 = 30.04.2007
|
||||
|
||||
my $day = ($value & 0b11111);
|
||||
my $month = (($value & 0b111100000000) >> 8);
|
||||
my $year = ((($value & 0b1111000000000000) >> 9) | (($value & 0b11100000) >> 5)) + 2000;
|
||||
if ($day > 31 || $month > 12 || $year > 2099) {
|
||||
return "invalid";
|
||||
} else {
|
||||
return $year . "-" . $month . "-" . $day;
|
||||
}
|
||||
}
|
||||
|
||||
sub valueCalcDateTime($$) {
|
||||
my $value = shift;
|
||||
my $dataBlock = shift;
|
||||
|
||||
#min: UI6 [1 to 6] <0 to 59>
|
||||
#hour: UI5 [9 to13] <0 to 23>
|
||||
#day: UI5 [17 to 21] <1 to 31>
|
||||
#month: UI4 [25 to 28] <1 to 12>
|
||||
#year: UI7[22 to 24,29 to 32] <0 to 99>
|
||||
# IV:
|
||||
# B1[8] {time invalid}:
|
||||
# IV<0> :=
|
||||
#valid,
|
||||
#IV>1> := invalid
|
||||
#SU: B1[16] {summer time}:
|
||||
#SU<0> := standard time,
|
||||
#SU<1> := summer time
|
||||
#RES1: B1[7] {reserved}: <0>
|
||||
#RES2: B1[14] {reserved}: <0>
|
||||
#RES3: B1[15] {reserved}: <0>
|
||||
|
||||
|
||||
my $datePart = $value >> 16;
|
||||
my $min = ($value & 0b111111);
|
||||
my $hour = ($value & 0b11111) >> 6;
|
||||
|
||||
return valueCalcDate($datePart, $dataBlock) . sprintf(' %02d:%02d', $hour, $min);
|
||||
}
|
||||
|
||||
sub valueCalcHex($$) {
|
||||
my $value = shift;
|
||||
my $dataBlock = shift;
|
||||
|
||||
return sprintf("%x", $value);
|
||||
}
|
||||
|
||||
# VIF types (Value Information Field), see page 32
|
||||
my %VIFInfo = (
|
||||
VIF_ELECTRIC_ENERGY => { # 10(nnn-3) Wh 0.001Wh to 10000Wh
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00000000,
|
||||
bias => -3,
|
||||
unit => 'Wh',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_THERMAL_ENERGY => { # 10(nnn) J 0.001kJ to 10000kJ
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00001000,
|
||||
bias => 0,
|
||||
unit => 'J',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_VOLUME => { # 10(nnn-6) m3 0.001l to 10000l
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00010000,
|
||||
bias => -6,
|
||||
unit => 'm³',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_MASS => { # 10(nnn-3) kg 0.001kg to 10000kg
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00011000,
|
||||
bias => -3,
|
||||
unit => 'kg',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_ON_TIME_SEC => { # seconds
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100000,
|
||||
bias => 0,
|
||||
unit => 'sec',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_ON_TIME_MIN => { # minutes
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100001,
|
||||
bias => 0,
|
||||
unit => 'min',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_ON_TIME_HOURS => { # hours
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100010,
|
||||
bias => 0,
|
||||
unit => 'hours',
|
||||
},
|
||||
VIF_ON_TIME_DAYS => { # days
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100011,
|
||||
bias => 0,
|
||||
unit => 'days',
|
||||
},
|
||||
VIF_OP_TIME_SEC => { # seconds
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100100,
|
||||
bias => 0,
|
||||
unit => 'sec',
|
||||
},
|
||||
VIF_OP_TIME_MIN => { # minutes
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100101,
|
||||
bias => 0,
|
||||
unit => 'min',
|
||||
},
|
||||
VIF_OP_TIME_HOURS => { # hours
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100110,
|
||||
bias => 0,
|
||||
unit => 'hours',
|
||||
},
|
||||
VIF_OP_TIME_DAYS => { # days
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00100111,
|
||||
bias => 0,
|
||||
unit => 'days',
|
||||
},
|
||||
VIF_ELECTRIC_POWER => { # 10(nnn-3) W 0.001W to 10000W
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00101000,
|
||||
bias => -3,
|
||||
unit => 'W',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_THERMAL_POWER => { # 10(nnn) J/h 0.001kJ/h to 10000kJ/h
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00110000,
|
||||
bias => 0,
|
||||
unit => 'J/h',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_VOLUME_FLOW => { # 10(nnn-6) m3/h 0.001l/h to 10000l/h
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b00111000,
|
||||
bias => -6,
|
||||
unit => 'm³/h',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_VOLUME_FLOW_EXT1 => { # 10(nnn-7) m3/min 0.0001l/min to 10000l/min
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b01000000,
|
||||
bias => -7,
|
||||
unit => 'm³/min',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_VOLUME_FLOW_EXT2 => { # 10(nnn-9) m3/s 0.001ml/s to 10000ml/s
|
||||
typeMask => 0b01111000,
|
||||
expMask => 0b00000111,
|
||||
type => 0b01001000,
|
||||
bias => -9,
|
||||
unit => 'm³/s',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_FLOW_TEMP => { # 10(nn-3) °C 0.001°C to 1°C
|
||||
typeMask => 0b01111100,
|
||||
expMask => 0b00000011,
|
||||
type => 0b01011000,
|
||||
bias => -3,
|
||||
unit => '°C',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_RETURN_TEMP => { # 10(nn-3) °C 0.001°C to 1°C
|
||||
typeMask => 0b01111100,
|
||||
expMask => 0b00000011,
|
||||
type => 0b01011100,
|
||||
bias => -3,
|
||||
unit => '°C',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_TEMP_DIFF => { # 10(nn-3) K 1mK to 1000mK
|
||||
typeMask => 0b01111100,
|
||||
expMask => 0b00000011,
|
||||
type => 0b01100000,
|
||||
bias => -3,
|
||||
unit => 'K',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_EXTERNAL_TEMP => { # 10(nn-3) °C 0.001°C to 1°C
|
||||
typeMask => 0b01111100,
|
||||
expMask => 0b00000011,
|
||||
type => 0b01100100,
|
||||
bias => -3,
|
||||
unit => '°C',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_TIME_POINT_DATE => { # data type G
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b01101100,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcDate,
|
||||
},
|
||||
VIF_TIME_POINT_DATE_TIME => { # data type F
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b01101101,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcDateTime,
|
||||
},
|
||||
VIF_HCA => { # dimensionless
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b01101110,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
);
|
||||
|
||||
# Codes used with extension indicator $FD
|
||||
my %VIFInfo_FD = (
|
||||
VIF_ACCESS_NO => { # Access number (transmission count)
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00001000,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_MODEL_VERSION => { # Model / Version
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00001100,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
VIF_ERROR_FLAGS => { # Error flags (binary)
|
||||
typeMask => 0b01111111,
|
||||
expMask => 0b00000000,
|
||||
type => 0b00010111,
|
||||
bias => 0,
|
||||
unit => '',
|
||||
calcFunc => \&valueCalcHex,
|
||||
},
|
||||
);
|
||||
|
||||
# Codes used with extension indicator $FB
|
||||
my %VIFInfo_FB = (
|
||||
VIF_ENERGY => { # Energy 10(n-1) MWh 0.1MWh to 1MWh
|
||||
typeMask => 0b01111110,
|
||||
expMask => 0b00000001,
|
||||
type => 0b00000000,
|
||||
bias => -1,
|
||||
unit => 'MWh',
|
||||
calcFunc => \&valueCalcNumeric,
|
||||
},
|
||||
);
|
||||
|
||||
# see 4.2.3, page 24
|
||||
my %validDeviceTypes = (
|
||||
0x00 => 'Other',
|
||||
0x01 => 'Oil',
|
||||
0x02 => 'Electricity',
|
||||
0x03 => 'Gas',
|
||||
0x04 => 'Heat',
|
||||
0x05 => 'Steam',
|
||||
0x06 => 'Warm Water (30 °C ... 90 °C)',
|
||||
0x07 => 'Water',
|
||||
0x08 => 'Heat Cost Allocator',
|
||||
0x09 => 'Compressed Air',
|
||||
0x0a => 'Cooling load meter (Volume measured at return temperature: outlet)',
|
||||
0x0b => 'Cooling load meter (Volume measured at flow temperature: inlet)',
|
||||
0x0c => 'Heat (Volume measured at flow temperature: inlet)',
|
||||
0x0d => 'Heat / Cooling load meter',
|
||||
0x0e => 'Bus / System component',
|
||||
0x0f => 'Unknown Medium',
|
||||
0x10 => 'Reserved for utility meter',
|
||||
0x11 => 'Reserved for utility meter',
|
||||
0x12 => 'Reserved for utility meter',
|
||||
0x13 => 'Reserved for utility meter',
|
||||
0x14 => 'Calorific value',
|
||||
0x15 => 'Hot water (> 90 °C)',
|
||||
0x16 => 'Cold water',
|
||||
0x17 => 'Dual register (hot/cold) Water meter',
|
||||
0x18 => 'Pressure',
|
||||
0x19 => 'A/D Converter',
|
||||
0x1a => 'Smokedetector',
|
||||
0x1b => 'Room sensor (e.g. temperature or humidity)',
|
||||
0x1c => 'Gasdetector'
|
||||
|
||||
);
|
||||
|
||||
|
||||
# bitfield, errors can be combined, see 4.2.3.2 on page 22
|
||||
my %validStates = (
|
||||
0x00 => 'no errors',
|
||||
0x01 => 'application busy',
|
||||
0x02 => 'any application error',
|
||||
0x03 => 'abnormal condition/alarm',
|
||||
0x04 => 'battery low',
|
||||
0x08 => 'permanent error',
|
||||
0x10 => 'temporary error',
|
||||
0x20 => 'specific to manufacturer',
|
||||
0x40 => 'specific to manufacturer',
|
||||
0x80 => 'specific to manufacturer',
|
||||
|
||||
);
|
||||
|
||||
my %encryptionModes = (
|
||||
0x00 => 'standard unsigned',
|
||||
0x01 => 'signed data telegram',
|
||||
0x02 => 'static telegram',
|
||||
0x03 => 'reserved',
|
||||
);
|
||||
|
||||
sub type2string($$) {
|
||||
my $class = shift;
|
||||
my $type = shift;
|
||||
|
||||
return $validDeviceTypes{$type} || 'unknown';
|
||||
}
|
||||
|
||||
sub state2string($$) {
|
||||
my $class = shift;
|
||||
my $state = shift;
|
||||
|
||||
my @result = ();
|
||||
|
||||
if ($state) {
|
||||
foreach my $stateMask ( keys %validStates ) {
|
||||
push @result, $validStates{$stateMask} if $state & $stateMask;
|
||||
}
|
||||
} else {
|
||||
@result = ($validStates{0});
|
||||
}
|
||||
return @result;
|
||||
}
|
||||
|
||||
|
||||
sub checkCRC($$) {
|
||||
my $self = shift;
|
||||
my $data = shift;
|
||||
my $ctx = Digest::CRC->new(width=>16, init=>0x0000, xorout=>0xffff, refout=>0, poly=>0x3D65, refin=>0, cont=>0);
|
||||
|
||||
$ctx->add($data);
|
||||
|
||||
return $ctx->digest;
|
||||
}
|
||||
|
||||
sub removeCRC($$)
|
||||
{
|
||||
my $self = shift;
|
||||
my $msg = shift;
|
||||
my $i;
|
||||
my $res;
|
||||
my $crc;
|
||||
|
||||
my $msgLen = length($msg);
|
||||
my $noOfBlocks = int($msgLen / 18);
|
||||
my $rest = $msgLen % 18;
|
||||
|
||||
|
||||
#print "Länge " . $msgLen . "\n";
|
||||
|
||||
for ($i=0; $i < $noOfBlocks; $i++) {
|
||||
|
||||
$crc = unpack('n',substr($msg, 18*$i+16, 2));
|
||||
#printf("%d: CRC %x, calc %x\n", $i, $crc, $self->checkCRC(substr($msg, 18*$i, 16)));
|
||||
if ($crc != $self->checkCRC(substr($msg, 18*$i, 16))) {
|
||||
$self->{errormsg} = "crc check failed for block $i";
|
||||
$self->{errorcode} = ERR_CRC_FAILED;
|
||||
return 0;
|
||||
}
|
||||
$res .= substr($msg, 18*$i, 16);
|
||||
}
|
||||
|
||||
if ($rest != 0) {
|
||||
$res .= substr($msg, $noOfBlocks*18, $rest - 2);
|
||||
$crc = unpack('n',substr($msg, $msgLen-2, 2));
|
||||
if ($crc != $self->checkCRC(substr($msg, $noOfBlocks*18, $rest - 2))) {
|
||||
$self->{errormsg} = "crc check failed for block $i";
|
||||
$self->{errorcode} = ERR_CRC_FAILED;
|
||||
#printf("rest %d: CRC %x, calc %x\n", $rest, $crc, $self->checkCRC(substr($msg, $noOfBlocks*18, $rest - 2)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
sub manId2hex($$)
|
||||
{
|
||||
my $self = shift;
|
||||
my $idascii = shift;
|
||||
|
||||
return (ord(substr($idascii,1,1))-64) << 10 | (ord(substr($idascii,2,1))-64) << 5 | (ord(substr($idascii,3,1))-64);
|
||||
}
|
||||
|
||||
sub manId2ascii($$)
|
||||
{
|
||||
my $self = shift;
|
||||
my $idhex = shift;
|
||||
|
||||
return chr(($idhex >> 10) + 64) . chr((($idhex >> 5) & 0b00011111) + 64) . chr(($idhex & 0b00011111) + 64);
|
||||
}
|
||||
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $self = {};
|
||||
bless $self, $class;
|
||||
|
||||
$self->_initialize();
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub _initialize {
|
||||
my $self = shift;
|
||||
|
||||
#$self->{dataBlocks} = [];
|
||||
}
|
||||
|
||||
sub decodeConfigword($) {
|
||||
my $self = shift;
|
||||
|
||||
$self->{cw_parts}{bidirectional} = $self->{cw} & 0b1000000000000000 >> 15;
|
||||
$self->{cw_parts}{accessability} = $self->{cw} & 0b0100000000000000 >> 14;
|
||||
$self->{cw_parts}{synchronous} = $self->{cw} & 0b0010000000000000 >> 13;
|
||||
$self->{cw_parts}{mode} = $self->{cw} & 0b0000111100000000 >> 8;
|
||||
$self->{cw_parts}{encrypted_blocks} = $self->{cw} & 0b0000000011110000 >> 4;
|
||||
$self->{cw_parts}{content} = $self->{cw} & 0b0000000000001100 >> 2;
|
||||
$self->{cw_parts}{hops} = $self->{cw} & 0b0000000000000011;
|
||||
}
|
||||
|
||||
sub decodeBCD($$$) {
|
||||
my $self = shift;
|
||||
my $digits = shift;
|
||||
my $bcd = shift;
|
||||
my $byte;
|
||||
my $val=0;
|
||||
my $mult=1;
|
||||
|
||||
for (my $i = 0; $i < $digits/2; $i++) {
|
||||
$byte = unpack('C',substr($bcd, $i, 1));
|
||||
$val += ($byte & 0x0f) * $mult;
|
||||
$mult *= 10;
|
||||
$val += (($byte & 0xf0) >> 4) * $mult;
|
||||
$mult *= 10;
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
sub decodeValueInformationBlock($$$) {
|
||||
my $self = shift;
|
||||
my $vib = shift;
|
||||
my $dataBlockRef = shift;
|
||||
|
||||
my $offset = 0;
|
||||
my $vif;
|
||||
my $bias;
|
||||
my $exponent;
|
||||
my $vifInfoRef = \%VIFInfo;
|
||||
|
||||
$vif = unpack('C', $vib);
|
||||
$offset = 1;
|
||||
|
||||
my $isExtension = $vif & VIF_EXTENSION_BIT;
|
||||
|
||||
if ($isExtension) {
|
||||
# switch to extension codes
|
||||
if ($vif == 0xFD) {
|
||||
$vifInfoRef = \%VIFInfo_FD;
|
||||
} elsif ($vif == 0xFB) {
|
||||
$vifInfoRef = \%VIFInfo_FB;
|
||||
} elsif ($vif == 0xFF) {
|
||||
# manufacturer specific data, can't be interpreted
|
||||
$dataBlockRef->{type} = "MANUFACTURER SPECIFIC";
|
||||
$dataBlockRef->{unit} = "";
|
||||
return $offset;
|
||||
} else {
|
||||
$self->{errormsg} = "unknown VIFE " . sprintf("%x", $vif) . " at offset $offset-1";
|
||||
$self->{errorcode} = ERR_UNKNOWN_VIFE;
|
||||
}
|
||||
$vif = unpack('C', substr($vib,$offset++,1));
|
||||
}
|
||||
|
||||
|
||||
$vif &= ~VIF_EXTENSION_BIT;
|
||||
|
||||
#printf("vif: %x\n", $vif);
|
||||
$dataBlockRef->{type} = '';
|
||||
VIFID: foreach my $vifType ( keys $vifInfoRef ) {
|
||||
|
||||
#printf "vifType $vifType\n";
|
||||
|
||||
if (($vif & $vifInfoRef->{$vifType}{typeMask}) == $vifInfoRef->{$vifType}{type}) {
|
||||
#printf "vifType $vifType matches\n";
|
||||
|
||||
$bias = $vifInfoRef->{$vifType}{bias};
|
||||
$exponent = $vif & $vifInfoRef->{$vifType}{expMask};
|
||||
|
||||
$dataBlockRef->{type} = $vifType;
|
||||
$dataBlockRef->{unit} = $vifInfoRef->{$vifType}{unit};
|
||||
$dataBlockRef->{valueFactor} = 10 ** ($exponent + $bias);
|
||||
$dataBlockRef->{calcFunc} = $vifInfoRef->{$vifType}{calcFunc};
|
||||
|
||||
#printf("type %s bias %d exp %d valueFactor %d unit %s\n", $dataBlockRef->{type}, $bias, $exponent, $dataBlockRef->{valueFactor},$dataBlockRef->{unit});
|
||||
last VIFID;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dataBlockRef->{type} eq '') {
|
||||
$self->{errormsg} = "unknown VIF " . sprintf("%x",$vif);
|
||||
$self->{errorcode} = ERR_UNKNOWN_VIF;
|
||||
}
|
||||
|
||||
return $offset;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
sub decodeDataInformationBlock($$$) {
|
||||
my $self = shift;
|
||||
my $dib = shift;
|
||||
my $dataBlockRef = shift;
|
||||
|
||||
my $dif;
|
||||
my $tariff = 0;
|
||||
my $difExtNo = 0;
|
||||
my $offset;
|
||||
my $devUnit = 0;
|
||||
|
||||
$dif = unpack('C', $dib);
|
||||
$offset = 1;
|
||||
my $isExtension = $dif & DIF_EXTENSION_BIT;
|
||||
my $storageNo = ($dif & 0b01000000) >> 6;
|
||||
my $functionField = ($dif & 0b00110000) >> 4;
|
||||
my $df = $dif & 0b00001111;
|
||||
|
||||
#printf("dif %x storage %d\n", $dif, $storageNo);
|
||||
|
||||
EXTENSION: while ($isExtension) {
|
||||
$dif = unpack('C', substr($dib,$offset,1));
|
||||
last EXTENSION if (!defined $dif);
|
||||
$offset++;
|
||||
$isExtension = $dif & DIF_EXTENSION_BIT;
|
||||
$difExtNo++;
|
||||
if ($difExtNo > 10) {
|
||||
$self->{errormsg} = 'too many DIFE';
|
||||
$self->{errorcode} = ERR_TOO_MANY_DIFE;
|
||||
last EXTENSION;
|
||||
}
|
||||
|
||||
$storageNo |= ($dif & 0b00001111) << ($difExtNo*4)+1;
|
||||
$tariff |= (($dif & 0b00110000 >> 4)) << (($difExtNo-1)*2);
|
||||
$devUnit |= (($dif & 0b01000000 >> 6)) << ($difExtNo-1);
|
||||
}
|
||||
|
||||
$dataBlockRef->{functionField} = $functionField;
|
||||
$dataBlockRef->{dataField} = $df;
|
||||
$dataBlockRef->{storageNo} = $storageNo;
|
||||
$dataBlockRef->{tariff} = $tariff;
|
||||
$dataBlockRef->{devUnit} = $devUnit;
|
||||
|
||||
#printf("in DIF: datafield %x\n", $dataBlockRef->{dataField});
|
||||
return $offset;
|
||||
}
|
||||
|
||||
sub decodeDataRecordHeader($$$) {
|
||||
my $self = shift;
|
||||
my $drh = shift;
|
||||
my $dataBlockRef = shift;
|
||||
|
||||
my $offset = $self->decodeDataInformationBlock($drh,$dataBlockRef);
|
||||
|
||||
|
||||
$offset += $self->decodeValueInformationBlock(substr($drh,$offset),$dataBlockRef);
|
||||
#printf("in DRH: type %s\n", $dataBlockRef->{type});
|
||||
|
||||
return $offset;
|
||||
}
|
||||
|
||||
|
||||
|
||||
sub decodePayload($$) {
|
||||
my $self = shift;
|
||||
my $payload = shift;
|
||||
my $offset = 0;
|
||||
my $dif;
|
||||
my $vif;
|
||||
my $scale;
|
||||
my $value;
|
||||
my $dataBlockNo = 0;
|
||||
|
||||
|
||||
my @dataBlocks = ();
|
||||
my $dataBlock;
|
||||
|
||||
|
||||
PAYLOAD: while ($offset < length($payload)) {
|
||||
$dataBlockNo++;
|
||||
|
||||
# create a new anonymous hash reference
|
||||
$dataBlock = {};
|
||||
$dataBlock->{number} = $dataBlockNo;
|
||||
|
||||
while (unpack('C',substr($payload,$offset,1)) == 0x2f) {
|
||||
# skip filler bytes
|
||||
#printf("skipping filler at offset %d of %d\n", $offset, length($payload));
|
||||
$offset++;
|
||||
if ($offset >= length($payload)) {
|
||||
last PAYLOAD;
|
||||
}
|
||||
}
|
||||
|
||||
$offset += $self->decodeDataRecordHeader(substr($payload,$offset), $dataBlock);
|
||||
|
||||
#printf("No. %d, type %x at offset %d\n", $dataBlockNo, $dataBlock->{dataField}, $offset-1);
|
||||
|
||||
if ($dataBlock->{dataField} == DIF_NONE || $dataBlock->{dataField} == DIF_READOUT) {
|
||||
} elsif ($dataBlock->{dataField} == DIF_BCD2) {
|
||||
$value = $self->decodeBCD(2, substr($payload,$offset,1));
|
||||
$offset += 1;
|
||||
} elsif ($dataBlock->{dataField} == DIF_BCD4) {
|
||||
$value = $self->decodeBCD(4, substr($payload,$offset,2));
|
||||
$offset += 2;
|
||||
} elsif ($dataBlock->{dataField} == DIF_BCD6) {
|
||||
$value = $self->decodeBCD(6, substr($payload,$offset,3));
|
||||
$offset += 3;
|
||||
} elsif ($dataBlock->{dataField} == DIF_BCD8) {
|
||||
$value = $self->decodeBCD(8, substr($payload,$offset,4));
|
||||
$offset += 4;
|
||||
} elsif ($dataBlock->{dataField} == DIF_INT8) {
|
||||
$value = unpack('C', substr($payload, $offset, 1));
|
||||
$offset += 1;
|
||||
} elsif ($dataBlock->{dataField} == DIF_INT16) {
|
||||
$value = unpack('v', substr($payload, $offset, 2));
|
||||
$offset += 2;
|
||||
} elsif ($dataBlock->{dataField} == DIF_INT32) {
|
||||
$value = unpack('V', substr($payload, $offset, 4));
|
||||
$offset += 4;
|
||||
} elsif ($dataBlock->{dataField} == DIF_VARLEN) {
|
||||
my $lvar = unpack('C',substr($payload, $offset++, 1));
|
||||
if ($lvar <= 0xbf) {
|
||||
# ASCII string with LVAR characters
|
||||
$value = unpack('a*',substr($payload, $offset, $lvar));
|
||||
$offset += $lvar;
|
||||
} elsif ($lvar >= 0xc0 && $lvar <= 0xcf) {
|
||||
# positive BCD number with (LVAR - C0h) • 2 digits
|
||||
$value = $self->decodeBCD(($lvar-0xc0)*2, substr($payload,$offset,($lvar-0xc0)));
|
||||
$offset += ($lvar-0xc0);
|
||||
} elsif ($lvar >= 0xd0 && $lvar <= 0xdf) {
|
||||
# negative BCD number with (LVAR - D0h) • 2 digits
|
||||
$value = -$self->decodeBCD(($lvar-0xd0)*2, substr($payload,$offset,($lvar-0xd0)));
|
||||
$offset += ($lvar-0xd0);
|
||||
} else {
|
||||
$self->{errormsg} = "in datablock $dataBlockNo: unhandled LVAR field " . sprintf("%x", $lvar);
|
||||
$self->{errorcode} = ERR_UNKNOWN_LVAR;
|
||||
return 0;
|
||||
}
|
||||
} elsif ($dataBlock->{dataField} == DIF_SPECIAL) {
|
||||
# special functions
|
||||
last PAYLOAD;
|
||||
} else {
|
||||
$self->{errormsg} = "in datablock $dataBlockNo: unhandled datafield " . sprintf("%x",$dataBlock->{dataField});
|
||||
$self->{errorcode} = ERR_UNKNOWN_DATAFIELD;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (defined $dataBlock->{calcFunc}) {
|
||||
$dataBlock->{value} = $dataBlock->{calcFunc}->($value, $dataBlock);
|
||||
#print "Value raw " . $value . " value calc " . $dataBlock->{value} ."\n";
|
||||
}
|
||||
|
||||
push @dataBlocks, $dataBlock;
|
||||
}
|
||||
|
||||
$self->{datablocks} = \@dataBlocks;
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub decrypt($) {
|
||||
my $self = shift;
|
||||
my $encrypted = shift;
|
||||
|
||||
# see 4.2.5.3, page 26
|
||||
my $initVector = substr($self->{msg},2,8);
|
||||
for (1..8) {
|
||||
$initVector .= pack('C',$self->{access_no});
|
||||
}
|
||||
my $cipher = Crypt::CBC->new(
|
||||
-key => $self->{aeskey},
|
||||
-cipher => "Crypt::OpenSSL::AES",
|
||||
-header => "none",
|
||||
-iv => $initVector,
|
||||
-literal_key => "true",
|
||||
-keysize => 16,
|
||||
);
|
||||
|
||||
return $cipher->decrypt($encrypted);
|
||||
}
|
||||
|
||||
sub decodeApplicationLayer($) {
|
||||
my $self = shift;
|
||||
my $applicationlayer = $self->removeCRC(substr($self->{msg},12));
|
||||
|
||||
if ($self->{errorcode} != ERR_NO_ERROR) {
|
||||
# CRC check failed
|
||||
return 0;
|
||||
}
|
||||
$self->{cifield} = unpack('C', $applicationlayer);
|
||||
|
||||
my $offset = 1;
|
||||
|
||||
if ($self->{cifield} == CI_RESP_4) {
|
||||
# Short header
|
||||
#print "short header\n";
|
||||
($self->{access_no}, $self->{status}, $self->{cw}) = unpack('CCn', substr($applicationlayer,$offset));
|
||||
$offset += 4;
|
||||
} elsif ($self->{cifield} == CI_RESP_12) {
|
||||
# Long header
|
||||
#print "Long header\n";
|
||||
($self->{meter_id}, $self->{meter_man}, $self->{meter_vers}, $self->{meter_dev}, $self->{access_no}, $self->{status}, $self->{cw})
|
||||
= unpack('VvCCCCn', substr($applicationlayer,$offset));
|
||||
$self->{meter_devtypestring} = $validDeviceTypes{$self->{meter_dev}};
|
||||
$self->{meter_manufacturer} = $self->manId2ascii($self->{meter_man});
|
||||
$offset += 12;
|
||||
} else {
|
||||
# unsupported
|
||||
$self->{errormsg} = 'Unsupported CI Field ' . sprintf("%x", $self->{cifield});
|
||||
$self->{errorcode} = ERR_UNKNOWN_CIFIELD;
|
||||
return 0;
|
||||
}
|
||||
$self->{statusstring} = join(", ", $self->state2string($self->{status}));
|
||||
|
||||
$self->decodeConfigword();
|
||||
|
||||
my $payload;
|
||||
|
||||
$self->{encryptionMode} = $encryptionModes{$self->{cw_parts}{mode}};
|
||||
if ($self->{cw_parts}{mode} == 0) {
|
||||
# no encryption
|
||||
$self->{isEncrypted} = 0;
|
||||
$self->{decrypted} = 1;
|
||||
$payload = substr($applicationlayer, $offset);
|
||||
} elsif ($self->{cw_parts}{mode} == 5) {
|
||||
# data is encrypted with AES 128, dynamic init vector
|
||||
# decrypt data before further processing
|
||||
$self->{isEncrypted} = 1;
|
||||
$self->{decrypted} = 0;
|
||||
|
||||
if ($self->{aeskey}) {
|
||||
$payload = $self->decrypt(substr($applicationlayer,$offset));
|
||||
if (unpack('n', $payload) == 0x2f2f) {
|
||||
$self->{decrypted} = 1;
|
||||
} else {
|
||||
# Decryption verification failed
|
||||
$self->{errormsg} = 'Decryption failed';
|
||||
$self->{errorcode} = ERR_DECRYPTION_FAILED;
|
||||
#printf("%x\n", unpack('n', $payload));
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
$self->{errormsg} = 'encrypted message and no aeskey given';
|
||||
$self->{errorcode} = ERR_NO_AESKEY;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} else {
|
||||
# error, encryption mode not implemented
|
||||
$self->{errormsg} = 'Encryption mode not implemented';
|
||||
$self->{errorcode} = ERR_UNKNOWN_ENCRYPTION;
|
||||
$self->{decrypted} = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $self->decodePayload($payload);
|
||||
|
||||
}
|
||||
|
||||
sub decodeLinkLayer($$)
|
||||
{
|
||||
my $self = shift;
|
||||
my $linklayer = shift;
|
||||
|
||||
$self->{datalen} = length($self->{msg}) - 12;
|
||||
$self->{datablocks} = $self->{datalen} / 18; # header block is 12 bytes, each following block is 16 bytes + 2 bytes CRC
|
||||
$self->{datablocks}++ if $self->{datalen} % 18 != 0;
|
||||
|
||||
|
||||
($self->{lfield}, $self->{cfield}, $self->{mfield}, $self->{afield_id}, $self->{afield_ver}, $self->{afield_type},
|
||||
$self->{crc0}) = unpack('CCvLCCn', $linklayer);
|
||||
|
||||
#printf("lfield %d\n", $self->{lfield});
|
||||
#printf("crc0 %x calc %x\n", $self->{crc0}, $self->checkCRC(substr($linklayer,0,10)));
|
||||
|
||||
if ($self->{crc0} != $self->checkCRC(substr($linklayer,0,10))) {
|
||||
$self->{errormsg} = "CRC check failed on link layer";
|
||||
$self->{errorcode} = ERR_CRC_FAILED;
|
||||
#print "CRC check failed on link layer\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
$self->{manufacturer} = $self->manId2ascii($self->{mfield});
|
||||
$self->{typestring} = $validDeviceTypes{$self->{afield_type}};
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sub parse($$)
|
||||
{
|
||||
my $self = shift;
|
||||
$self->{msg} = shift;
|
||||
|
||||
$self->{errormsg} = '';
|
||||
$self->{errorcode} = ERR_NO_ERROR;
|
||||
if ($self->decodeLinkLayer(substr($self->{msg},0,12)) != 0) {
|
||||
$self->{linkLayerOk} = 1;
|
||||
return $self->decodeApplicationLayer();
|
||||
}
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
sub parseLinkLayer($$)
|
||||
{
|
||||
my $self = shift;
|
||||
$self->{msg} = shift;
|
||||
|
||||
$self->{errormsg} = '';
|
||||
$self->{errorcode} = ERR_NO_ERROR;
|
||||
$self->{linkLayerOk} = $self->decodeLinkLayer(substr($self->{msg},0,12));
|
||||
return $self->{linkLayerOk};
|
||||
}
|
||||
|
||||
sub parseApplicationLayer($)
|
||||
{
|
||||
my $self = shift;
|
||||
|
||||
$self->{errormsg} = '';
|
||||
$self->{errorcode} = ERR_NO_ERROR;
|
||||
return $self->decodeApplicationLayer();
|
||||
}
|
||||
|
||||
1;
|
@ -117,6 +117,7 @@ FHEM/36_PCA301.pm justme1968 http://forum.fhem.de Sonstige
|
||||
FHEM/36_LaCrosse.pm justme1968 http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/36_EMT7110.pm HCS http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/36_Level.pm HCS http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/36_WMBUS.pm kaihs http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/37_SHC.pm rr2000 http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/37_SHCdev.pm rr2000 http://forum.fhem.de Sonstige Systeme
|
||||
FHEM/38_CO20.pm justme1968 http://forum.fhem.de Sonstiges
|
||||
|
Loading…
x
Reference in New Issue
Block a user