diff --git a/fhem/FHEM/36_WMBUS.pm b/fhem/FHEM/36_WMBUS.pm
index eec6b033a..e88e0b3b8 100644
--- a/fhem/FHEM/36_WMBUS.pm
+++ b/fhem/FHEM/36_WMBUS.pm
@@ -601,14 +601,14 @@ WMBUS_Attr(@)
Therefor each semantically different value will get a unique reading name.
Example:
- 1_storage_no 0
- 1_type VIF_ENERGY_WATT
- 1_unit Wh
+ 1_storage_no 0
+ 1_type VIF_ENERGY_WATT
+ 1_unit Wh
1_value 1234.5
will be changed to
- 0_VIF_ENERGY_WATT_unit Wh
+ 0_VIF_ENERGY_WATT_unit Wh
0_VIF_ENERGY_WATT_value 1234.5
@@ -749,14 +749,14 @@ WMBUS_Attr(@)
VIF (Value Information Field) zusammen. Dadurch bekommt jeder semantisch unterschiedliche Wert einen eindeutigen Readingnamen.
Beispiel:
- 1_storage_no 0
- 1_type VIF_ENERGY_WATT
- 1_unit Wh
+ 1_storage_no 0
+ 1_type VIF_ENERGY_WATT
+ 1_unit Wh
1_value 1234.5
wird zu
- 0_VIF_ENERGY_WATT_unit Wh
+ 0_VIF_ENERGY_WATT_unit Wh
0_VIF_ENERGY_WATT_value 1234.5
diff --git a/fhem/FHEM/WMBus.pm b/fhem/FHEM/WMBus.pm
index da930e6f4..1da7d2d89 100644
--- a/fhem/FHEM/WMBus.pm
+++ b/fhem/FHEM/WMBus.pm
@@ -1,2490 +1,2490 @@
-# $Id$
-
-package WMBus;
-
-use strict;
-use warnings;
-use feature qw(say);
-use Scalar::Util qw(looks_like_number);
-use Digest::CRC; # libdigest-crc-perl
-eval "use Crypt::Mode::CBC"; # cpan -i Crypt::Mode::CBC
-my $hasCBC = ($@)?0:1;
-eval "use Crypt::Mode::CTR"; # cpan -i Crypt::Mode::CTR
-my $hasCTR = ($@)?0:1;
-eval "use Digest::CMAC"; # cpan -i Digest::CMAC
-my $hasCMAC = ($@)?0:1;
-
-require Exporter;
-my @ISA = qw(Exporter);
-my @EXPORT = qw(new parse parseLinkLayer parseApplicationLayer manId2ascii type2string setFrameType getFrameType VIF_TYPE_MANUFACTURER_SPECIFIC);
-
-sub manId2ascii($$);
-
-
-use constant {
- # Transport Layer block size
- TL_BLOCK_SIZE => 10,
- # Link Layer block size
- LL_BLOCK_SIZE => 16,
- # size of CRC in bytes
- CRC_SIZE => 2,
-
- # 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
- CI_RESP_0 => 0x78, # Response from device, 0 Byte header, variable length
- CI_ERROR => 0x70, # Error from device, only specified for wired M-Bus but used by Easymeter WMBUS module
- CI_TL_4 => 0x8a, # Transport layer from device, 4 Bytes
- CI_TL_12 => 0x8b, # Transport layer from device, 12 Bytes
- CI_ELL_2 => 0x8c, # Extended Link Layer, 2 Bytes
- CI_ELL_10 => 0x8e, # Extended Link Layer, 10 Bytes
- CI_ELL_8 => 0x8d, # Extended Link Layer, 8 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4)
- CI_ELL_16 => 0x8f, # Extended Link Layer, 16 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4)
- CI_AFL => 0x90, # Authentification and Fragmentation Layer, variable size
- CI_RESP_SML_4 => 0x7e, # Response from device, 4 Bytes, application layer SML encoded
- CI_RESP_SML_12 => 0x7f, # Response from device, 12 Bytes, application layer SML encoded
- CI_SND_UD_MODE_1 => 0x51, # The master can send data to a slave using a SND_UD with CI-Field 51h for mode 1 or 55h for mode 2
- CI_SND_UD_MODE_2 => 0x55,
-
- # DIF types (Data Information Field), see page 32
- DIF_NONE => 0x00,
- DIF_INT8 => 0x01,
- DIF_INT16 => 0x02,
- DIF_INT24 => 0x03,
- 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,
- ERR_TOO_MANY_VIFE => 11,
- ERR_MSG_TOO_SHORT => 12,
- ERR_SML_PAYLOAD => 13,
- ERR_FRAGMENT_UNSUPPORTED => 14,
- ERR_UNKNOWN_COMPACT_FORMAT => 15,
- ERR_CIPHER_NOT_INSTALLED => 16,
- ERR_LINK_LAYER_INVALID => 17,
-
- VIF_TYPE_MANUFACTURER_SPECIFIC => 'MANUFACTURER SPECIFIC',
-
- # TYPE C transmission uses two different frame types
- # see http://www.st.com/content/ccc/resource/technical/document/application_note/3f/fb/35/5a/25/4e/41/ba/DM00233038.pdf/files/DM00233038.pdf/jcr:content/translations/en.DM00233038.pdf
- FRAME_TYPE_A => 'A',
- FRAME_TYPE_B => 'B',
-
- # content type (CC bits of configuration field)
- # stored in $self->{cw_parts}{content}
- CONTENT_STANDARD => 0b00, # Standard data message with unsigned variable meter data
- CONTENT_STATIC => 0b10, # Static message (consists of parameter, OBIS definitions and other data points
- # which are not frequently changed – see also 4.3.2.4).
-
-
-};
-
-sub valueCalcNumeric($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- # some sanity checks on the provided data
- if (defined($value) && defined($dataBlock->{valueFactor}) && looks_like_number($value))
- {
- return $value * $dataBlock->{valueFactor};
- } else {
- return 0;
- }
-
-}
-
-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 sprintf("invalid: %x", $value);
- } else {
- return sprintf("%04d-%02d-%02d", $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 $timeInvalid = $value & 0b10000000;
-
- my $dateTime = valueCalcDate($datePart, $dataBlock);
- if ($timeInvalid == 0) {
- my $min = ($value & 0b111111);
- my $hour = ($value >> 8) & 0b11111;
- my $su = ($value & 0b1000000000000000);
- if ($min > 59 || $hour > 23) {
- $dateTime = sprintf('invalid: %x', $value);
- } else {
- $dateTime .= sprintf(' %02d:%02d %s', $hour, $min, $su ? 'DST' : '');
- }
- }
-
- return $dateTime;
-}
-
-sub valueCalcHex($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- return unpack("H*", $value);
-}
-
-sub valueCalcAscii($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- my $result = unpack('a*',$value);
-
- # replace non printable chars
- $result =~ s/[\x00-\x1f\x7f-\xff]/?/g;
-
- return $result;
-}
-
-sub valueCalcu($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- my $result = '';
-
- $result = ($value & 0b00001000 ? 'upper' : 'lower') . ' limit';
- return $result;
-}
-
-sub valueCalcufnn($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- my $result = '';
-
- $result = ($value & 0b00001000 ? 'upper' : 'lower') . ' limit';
- $result .= ', ' . ($value & 0b00000100 ? 'first' : 'last');
- $result .= sprintf(', duration %d', $value & 0b11);
- return $result;
-}
-
-sub valueCalcMultCorr1000($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- $dataBlock->{value} *= 1000;
-
- return "correction by factor 1000";
-}
-
-
-my %TimeSpec = (
- 0b00 => 's', # seconds
- 0b01 => 'm', # minutes
- 0b10 => 'h', # hours
- 0b11 => 'd', # days
-);
-
-sub valueCalcTimeperiod($$) {
- my $value = shift;
- my $dataBlock = shift;
-
- $dataBlock->{unit} = $TimeSpec{$dataBlock->{exponent}};
- return $value;
-}
-
-# VIF types (Value Information Field), see page 32
-my %VIFInfo = (
- VIF_ENERGY_WATT => { # 10(nnn-3) Wh 0.001Wh to 10000Wh
- typeMask => 0b01111000,
- expMask => 0b00000111,
- type => 0b00000000,
- bias => -3,
- unit => 'Wh',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_ENERGY_JOULE => { # 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_MASS_FLOW => { # 10(nnn-3) kg/h 0.001kg/h to 10000kg/h
- typeMask => 0b01111000,
- expMask => 0b00000111,
- type => 0b01010000,
- bias => -3,
- unit => 'kg/h',
- 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_PRESSURE => { # 10(nn-3) bar 1mbar to 1000mbar
- typeMask => 0b01111100,
- expMask => 0b00000011,
- type => 0b01101000,
- bias => -3,
- unit => 'bar',
- 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 => { # Unit for Heat Cost Allocator, dimensonless
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01101110,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_FABRICATION_NO => { # Fabrication No
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01111000,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcAscii,
- },
- VIF_OWNER_NO => { # Eigentumsnummer (used by Easymeter even though the standard allows this only for writing to a slave)
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01111001,
- bias => 0,
- unit => '',
- },
- VIF_AVERAGING_DURATION_SEC => { # seconds
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110000,
- bias => 0,
- unit => 'sec',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_AVERAGING_DURATION_MIN => { # minutes
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110001,
- bias => 0,
- unit => 'min',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_AVERAGING_DURATION_HOURS => { # hours
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110010,
- bias => 0,
- unit => 'hours',
- },
- VIF_AVERAGING_DURATION_DAYS => { # days
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110011,
- bias => 0,
- unit => 'days',
- },
- VIF_ACTUALITY_DURATION_SEC => { # seconds
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110100,
- bias => 0,
- unit => 'sec',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_ACTUALITY_DURATION_MIN => { # minutes
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110101,
- bias => 0,
- unit => 'min',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_ACTUALITY_DURATION_HOURS => { # hours
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110110,
- bias => 0,
- unit => 'hours',
- },
- VIF_ACTUALITY_DURATION_DAYS => { # days
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110111,
- bias => 0,
- unit => 'days',
- },
-);
-
-# Codes used with extension indicator $FD, see 8.4.4 on page 80
-my %VIFInfo_FD = (
- VIF_CREDIT => { # Credit of 10nn-3 of the nominal local legal currency units
- typeMask => 0b01111100,
- expMask => 0b00000011,
- type => 0b00000000,
- bias => -3,
- unit => '€',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_DEBIT => { # Debit of 10nn-3 of the nominal local legal currency units
- typeMask => 0b01111100,
- expMask => 0b00000011,
- type => 0b00000100,
- bias => -3,
- unit => '€',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_ACCESS_NO => { # Access number (transmission count)
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001000,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_MEDIUM => { # Medium (as in fixed header)
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001001,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcAscii,
- },
- VIF_MANUFACTURER => { # Manufacturer (as in fixed header)
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001010,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_PARAMETER_SET_ID => { # Parameter set identification
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001011,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_MODEL_VERSION => { # Model / Version
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001100,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_HARDWARE_VERSION => { # Hardware version #
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001101,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_FIRMWARE_VERSION => { # Firmware version #
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001110,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_SOFTWARE_VERSION => { # Software version #
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001111,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
-
-
- VIF_CUSTOMER_LOCATION => { # Customer location
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010000,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
- VIF_CUSTOMER_CUSTOMER => { # Customer
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010001,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
- VIF_ACCESS_CODE_USER => { # Access code user
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010010,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
- VIF_ACCESS_CODE_OPERATOR => { # Access code operator
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010011,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
- VIF_ACCESS_CODE_SYSTEM_OPERATOR => { # Access code system operator
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010100,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
- VIF_PASSWORD => { # Password
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010110,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex
- },
-
- VIF_ERROR_FLAGS => { # Error flags (binary)
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010111,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcHex,
- },
- VIF_DURATION_SINCE_LAST_READOUT => { # Duration since last readout [sec(s)..day(s)]
- typeMask => 0b01111100,
- expMask => 0b00000011,
- type => 0b00101100,
- bias => 0,
- unit => 's',
- calcFunc => \&valueCalcTimeperiod,
- },
- VIF_VOLTAGE => { # 10nnnn-9 Volts
- typeMask => 0b01110000,
- expMask => 0b00001111,
- type => 0b01000000,
- bias => -9,
- unit => 'V',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_ELECTRICAL_CURRENT => { # 10nnnn-12 Ampere
- typeMask => 0b01110000,
- expMask => 0b00001111,
- type => 0b01010000,
- bias => -12,
- unit => 'A',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_RECEPTION_LEVEL => { # reception level of a received radio device.
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01110001,
- bias => 0,
- unit => 'dBm',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_STATE_PARAMETER_ACTIVATION => { # State of parameter activation
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01100110,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_SPECIAL_SUPPLIER_INFORMATION => { # Special supplier information
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01100111,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcNumeric,
- },
-
- VIF_FD_RESERVED => { # Reserved
- typeMask => 0b01110000,
- expMask => 0b00000000,
- type => 0b01110000,
- bias => 0,
- unit => 'Reserved',
- },
-
-);
-
-# 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,
- },
-);
-
-
-# Codes used for an enhancement of VIFs other than $FD and $FB
-my %VIFInfo_other = (
- VIF_ERROR_NONE => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00000000,
- bias => 0,
- unit => 'No error',
- },
- VIF_TOO_MANY_DIFES => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00000001,
- bias => 0,
- unit => 'Too many DIFEs',
- },
-
- VIF_ILLEGAL_VIF_GROUP => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00001100,
- bias => 0,
- unit => 'Illegal VIF-Group',
- },
-
- VIF_DATA_UNDERFLOW => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00010111,
- bias => 0,
- unit => 'Data underflow',
- },
-
-
- VIF_PER_SECOND => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100000,
- bias => 0,
- unit => 'per second',
- },
- VIF_PER_MINUTE => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100001,
- bias => 0,
- unit => 'per minute',
- },
- VIF_PER_HOUR => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100010,
- bias => 0,
- unit => 'per hour',
- },
- VIF_PER_DAY => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100011,
- bias => 0,
- unit => 'per day',
- },
- VIF_PER_WEEK => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100100,
- bias => 0,
- unit => 'per week',
- },
- VIF_PER_MONTH => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100101,
- bias => 0,
- unit => 'per month',
- },
- VIF_PER_YEAR => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100110,
- bias => 0,
- unit => 'per year',
- },
- VIF_PER_REVOLUTION => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00100111,
- bias => 0,
- unit => 'per revolution/measurement',
- },
- VIF_PER_INCREMENT_INPUT => {
- typeMask => 0b01111110,
- expMask => 0b00000000,
- type => 0b00101000,
- bias => 0,
- unit => 'increment per input pulse on input channnel #',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_PER_INCREMENT_OUTPUT => {
- typeMask => 0b01111110,
- expMask => 0b00000000,
- type => 0b00101010,
- bias => 0,
- unit => 'increment per output pulse on output channnel #',
- calcFunc => \&valueCalcNumeric,
- },
- VIF_PER_LITER => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00101100,
- bias => 0,
- unit => 'per liter',
- },
- VIF_PER_M3 => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00101101,
- bias => 0,
- unit => 'per m³',
- },
- VIF_PER_KG => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00101110,
- bias => 0,
- unit => 'per kg',
- },
- VIF_PER_K => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00101111,
- bias => 0,
- unit => 'per K',
- },
- VIF_PER_KWH => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110000,
- bias => 0,
- unit => 'per kWh',
- },
- VIF_PER_GJ => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110001,
- bias => 0,
- unit => 'per GJ',
- },
- VIF_PER_KW => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110010,
- bias => 0,
- unit => 'per kW',
- },
- VIF_PER_KL => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110011,
- bias => 0,
- unit => 'per (K*l)',
- },
- VIF_PER_V => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110100,
- bias => 0,
- unit => 'per V',
- },
- VIF_PER_A => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110101,
- bias => 0,
- unit => 'per A',
- },
- VIF_PER_MULT_S => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110110,
- bias => 0,
- unit => 'multiplied by sek',
- },
- VIF_PER_MULT_SV => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00110111,
- bias => 0,
- unit => 'multiplied by sek / V',
- },
- VIF_PER_MULT_SA => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00111000,
- bias => 0,
- unit => 'multiplied by sek / A',
- },
-
- VIF_START_DATE_TIME => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00111001,
- bias => 0,
- unit => 'start date(/time) of',
- },
-
- VIF_ACCUMULATION_IF_POSITIVE => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b00111011,
- bias => 0,
- unit => 'Accumulation only if positive contribution',
- },
-
- VIF_DURATION_NO_EXCEEDS => {
- typeMask => 0b01110111,
- expMask => 0b00000000,
- type => 0b01000001,
- bias => 0,
- unit => '# of exceeds',
- calcFunc => \&valueCalcu,
- },
-
- VIF_DURATION_LIMIT_EXCEEDED => {
- typeMask => 0b01110000,
- expMask => 0b00000000,
- type => 0b01010000,
- bias => 0,
- unit => 'duration of limit exceeded',
- calcFunc => \&valueCalcufnn,
- },
-
- VIF_MULTIPLICATIVE_CORRECTION_FACTOR => {
- typeMask => 0b01111000,
- expMask => 0b00000111,
- type => 0b01110000,
- bias => -6,
- unit => '',
- },
- VIF_MULTIPLICATIVE_CORRECTION_FACTOR_1000 => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01111101,
- bias => 0,
- unit => '',
- calcFunc => \&valueCalcMultCorr1000,
- },
- VIF_FUTURE_VALUE => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01111110,
- bias => 0,
- unit => '',
- },
- VIF_MANUFACTURER_SPECIFIC => {
- typeMask => 0b01111111,
- expMask => 0b00000000,
- type => 0b01111111,
- bias => 0,
- unit => 'manufacturer specific',
- },
-
-);
-
-# For Easymeter (manufacturer specific)
-my %VIFInfo_ESY = (
- VIF_ELECTRIC_POWER_PHASE => {
- typeMask => 0b01000000,
- expMask => 0b00000000,
- type => 0b00000000,
- bias => -2,
- unit => 'W',
- calcFunc => \&valueCalcNumeric,
- },
-);
-
-my %VIFInfo_ESY2 = (
- VIF_ELECTRIC_POWER_PHASE_NO => {
- typeMask => 0b01111110,
- expMask => 0b00000000,
- type => 0b00101000,
- bias => 0,
- unit => 'phase #',
- calcFunc => \&valueCalcNumeric,
- },
-);
-
-# For Kamstrup (manufacturer specific)
-my %VIFInfo_KAM = (
- VIF_KAMSTRUP_INFO => {
- typeMask => 0b00000000,
- expMask => 0b00000000,
- type => 0b00000000,
- bias => 0,
- unit => '',
- },
-);
-
-
-# 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',
- 0x1d => 'Reserved for sensors',
- 0x1e => 'Reserved for sensors',
- 0x1f => 'Reserved for sensors',
- 0x20 => 'Breaker (electricity)',
- 0x21 => 'Valve (gas)',
- 0x22 => 'Reserved for switching devices',
- 0x23 => 'Reserved for switching devices',
- 0x24 => 'Reserved for switching devices',
- 0x25 => 'Customer unit (Display device)',
- 0x26 => 'Reserved for customer units',
- 0x27 => 'Reserved for customer units',
- 0x28 => 'Waste water',
- 0x29 => 'Garbage',
- 0x2a => 'Carbon dioxide',
- 0x2b => 'Environmental meter',
- 0x2c => 'Environmental meter',
- 0x2d => 'Environmental meter',
- 0x2e => 'Environmental meter',
- 0x2f => 'Environmental meter',
- 0x31 => 'OMS MUC',
- 0x32 => 'OMS unidirectional repeater',
- 0x33 => 'OMS bidirectional repeater',
- 0x37 => 'Radio converter (Meter side)',
-);
-
-
-# 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',
-);
-
-my %functionFieldTypes = (
- 0b00 => 'Instantaneous value',
- 0b01 => 'Maximum value',
- 0b10 => 'Minimum value',
- 0b11 => 'Value during error state',
-);
-
-
-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 calcCRC($$) {
- 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 $blocksize = LL_BLOCK_SIZE;
- my $blocksize_with_crc = LL_BLOCK_SIZE + $self->{crc_size};
- my $crcoffset;
-
- my $msgLen = $self->{datalen}; # size without CRCs
- my $noOfBlocks = $self->{datablocks}; # total number of data blocks, each with a CRC appended
- my $rest = $msgLen % LL_BLOCK_SIZE; # size of the last data block, can be smaller than 16 bytes
-
-
- #print "crc_size $self->{crc_size}\n";
-
- return $msg if $self->{crc_size} == 0;
-
- # each block is 16 bytes + 2 bytes CRC
-
- #print "Länge $msgLen Anz. Blöcke $noOfBlocks rest $rest\n";
-
- for ($i=0; $i < $noOfBlocks; $i++) {
- $crcoffset = $blocksize_with_crc * $i + LL_BLOCK_SIZE;
- #print "$i: crc offset $crcoffset\n";
- if ($rest > 0 && $crcoffset + $self->{crc_size} > ($noOfBlocks - 1) * $blocksize_with_crc + $rest) {
- # last block is smaller
- $crcoffset = ($noOfBlocks - 1) * $blocksize_with_crc + $rest;
- #print "last crc offset $crcoffset\n";
- $blocksize = $msgLen - ($i * $blocksize);
- }
-
- $crc = unpack('n',substr($msg, $crcoffset, $self->{crc_size}));
- #printf("%d: CRC %x, calc %x blocksize $blocksize\n", $i, $crc, $self->calcCRC(substr($msg, $blocksize_with_crc*$i, $blocksize)));
- if ($crc != $self->calcCRC(substr($msg, $blocksize_with_crc*$i, $blocksize))) {
- $self->{errormsg} = "crc check failed for block $i";
- $self->{errorcode} = ERR_CRC_FAILED;
- return 0;
- }
- $res .= substr($msg, $blocksize_with_crc*$i, $blocksize);
- }
-
- return $res;
-}
-
-
-sub manId2hex($$)
-{
- my $class = 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 $class = 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->{crc_size} = CRC_SIZE;
- $self->{frame_type} = FRAME_TYPE_A; # default
-}
-
-sub setCRCsize {
- my $self = shift;
-
- $self->{crc_size} = shift;
-}
-
-sub getCRCsize {
- my $self = shift;
-
- return $self->{crc_size};
-}
-
-sub decodeConfigword($) {
- my $self = shift;
-
-
- #printf("cw: %01x %01x\n", $self->{cw_1}, $self->{cw_2});
- $self->{cw_parts}{mode} = ($self->{cw_2} & 0b00011111);
- #printf("mode: %02x\n", $self->{cw_parts}{mode});
- if ($self->{cw_parts}{mode} == 5 || $self->{cw_parts}{mode} == 0) {
- $self->{cw_parts}{bidirectional} = ($self->{cw_2} & 0b10000000) >> 7;
- $self->{cw_parts}{accessability} = ($self->{cw_2} & 0b01000000) >> 6;
- $self->{cw_parts}{synchronous} = ($self->{cw_2} & 0b00100000) >> 5;
- $self->{cw_parts}{encrypted_blocks} = ($self->{cw_1} & 0b11110000) >> 4;
- $self->{cw_parts}{content} = ($self->{cw_1} & 0b00001100) >> 2;
- $self->{cw_parts}{repeated_access} = ($self->{cw_1} & 0b00000010) >> 1;
- $self->{cw_parts}{hops} = ($self->{cw_1} & 0b00000001);
- } elsif ($self->{cw_parts}{mode} == 7) {
- # configword ist 3 Bytes lang
- $self->{cw_parts}{key_id} = ($self->{cw_3} & 0b00001111);
- $self->{cw_parts}{dynamic_key} = ($self->{cw_3} & 0b01110000) >> 4;
- $self->{cw_parts}{content} = ($self->{cw_2} & 0b11000000) >> 6;
- $self->{cw_parts}{encrypted_blocks} = ($self->{cw_1} & 0b11110000) >> 4;
- }
-}
-
-sub decodeBCD($$$) {
- my $self = shift;
- my $digits = shift;
- my $bcd = shift;
- my $byte;
- my $val=0;
- my $mult=1;
-
- #print "bcd:" . unpack("H*", $bcd) . "\n";
-
- 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 findVIF($$$) {
- my $vif = shift;
- my $vifInfoRef = shift;
- my $dataBlockRef = shift;
- my $bias;
-
- if (defined $vifInfoRef) {
- VIFID: foreach my $vifType ( keys %$vifInfoRef ) {
-
- #printf "vifType $vifType VIF $vif typeMask $vifInfoRef->{$vifType}{typeMask} type $vifInfoRef->{$vifType}{type}\n";
-
- if (($vif & $vifInfoRef->{$vifType}{typeMask}) == $vifInfoRef->{$vifType}{type}) {
- #printf " match vif %02x vifType %s\n", $vif, $vifType;
- $dataBlockRef->{vif} = $vif;
-
- $bias = $vifInfoRef->{$vifType}{bias};
- $dataBlockRef->{exponent} = $vif & $vifInfoRef->{$vifType}{expMask};
-
- $dataBlockRef->{type} = $vifType;
- $dataBlockRef->{unit} = $vifInfoRef->{$vifType}{unit};
- if (defined $dataBlockRef->{exponent} && defined $bias) {
- $dataBlockRef->{valueFactor} = 10 ** ($dataBlockRef->{exponent} + $bias);
- } else {
- $dataBlockRef->{valueFactor} = 1;
- }
- $dataBlockRef->{calcFunc} = $vifInfoRef->{$vifType}{calcFunc};
-
- #printf("type %s bias %d exp %d valueFactor %d unit %s\n", $dataBlockRef->{type}, $bias, $dataBlockRef->{exponent}, $dataBlockRef->{valueFactor},$dataBlockRef->{unit});
- return 1;
- }
- }
- #printf "no match!\n";
- return 0;
- }
- return 1;
-}
-
-sub decodePlaintext($$$) {
- my $self = shift;
- my $vib = shift;
- my $dataBlockRef = shift;
- my $offset = shift;
- my $vifLength = unpack('C', substr($vib,$offset++,1));
-
- $dataBlockRef->{type} = "see unit";
- $dataBlockRef->{unit} = substr($vib, $offset, $vifLength);
- $dataBlockRef->{unit} = reverse($dataBlockRef->{unit}) unless $self->{mode_bit};
- $offset += $vifLength;
- return $offset;
-}
-
-sub decodeValueInformationBlock($$$) {
- my $self = shift;
- my $vib = shift;
- my $dataBlockRef = shift;
-
- my $offset = 0;
- my $vif;
- my $vifInfoRef;
- my $vifExtension = 0;
- my $vifExtNo = 0;
- my $isExtension;
- my $dataBlockExt;
- my @VIFExtensions = ();
- my $analyzeVIF = 1;
-
- $dataBlockRef->{type} = '';
- # The unit and multiplier is taken from the table for primary VIF
- $vifInfoRef = \%VIFInfo;
-
-
- EXTENSION: while (1) {
- $vif = unpack('C', substr($vib,$offset++,1));
- $isExtension = $vif & VIF_EXTENSION_BIT;
- #printf("vif: %x isExtension %d\n", $vif, $isExtension);
- if ($isExtension) {
- $dataBlockRef->{vif} = $vif;
- }
-
- # Is this an extension?
- last EXTENSION if (!$isExtension);
-
- # yes, process extension
-
- $vifExtNo++;
- if ($vifExtNo > 10) {
- $dataBlockRef->{errormsg} = 'too many VIFE';
- $dataBlockRef->{errorcode} = ERR_TOO_MANY_VIFE;
- last EXTENSION;
- }
-
- # switch to extension codes
- $vifExtension = $vif;
- $vif &= ~VIF_EXTENSION_BIT;
- #printf("vif ohne extension: %x\n", $vif);
- if ($vif == 0x7D) {
- $vifInfoRef = \%VIFInfo_FD;
- } elsif ($vif == 0x7B) {
- $vifInfoRef = \%VIFInfo_FB;
- } elsif ($vif == 0x7C) {
- # Plaintext VIF
- $offset = $self->decodePlaintext($vib, $dataBlockRef, $offset);
- $analyzeVIF = 0;
- last EXTENSION;
- } elsif ($vif == 0x7F) {
-
- if ($self->{manufacturer} eq 'ESY') {
- # Easymeter
- $vif = unpack('C', substr($vib,$offset++,1));
- #printf("ESY VIF %x\n", $vif);
- $vifInfoRef = \%VIFInfo_ESY;
- } elsif ($self->{manufacturer} eq 'KAM') {
- # Kamstrup
- $vif = unpack('C', substr($vib,$offset++,1));
- $vifInfoRef = \%VIFInfo_KAM;
- } else {
- # manufacturer specific data, can't be interpreted
-
- $dataBlockRef->{type} = VIF_TYPE_MANUFACTURER_SPECIFIC;
- $dataBlockRef->{unit} = "";
- $analyzeVIF = 0;
- }
- last EXTENSION;
- } else {
- # enhancement of VIFs other than $FD and $FB (see page 84ff.)
- #print "other extension\n";
- $dataBlockExt = {};
- if ($self->{manufacturer} eq 'ESY') {
- #print "ESY\n";
- $vifInfoRef = \%VIFInfo_ESY2;
- $dataBlockExt->{value} = unpack('C',substr($vib,2,1)) * 100;
- } else {
- $dataBlockExt->{value} = $vif;
- $vifInfoRef = \%VIFInfo_other;
- }
-
- if (findVIF($vif, $vifInfoRef, $dataBlockExt)) {
- push @VIFExtensions, $dataBlockExt;
- } else {
- $dataBlockRef->{type} = 'unknown';
- $dataBlockRef->{errormsg} = "unknown VIFE " . sprintf("%x", $vifExtension) . " at offset " . ($offset-1);
- $dataBlockRef->{errorcode} = ERR_UNKNOWN_VIFE;
- }
- }
- last EXTENSION if (!$isExtension);
- }
-
- if ($analyzeVIF) {
- if ($vif == 0x7C) {
- # Plaintext VIF
- $offset = $self->decodePlaintext($vib, $dataBlockRef, $offset);
- } elsif (findVIF($vif, $vifInfoRef, $dataBlockRef) == 0) {
- $dataBlockRef->{errormsg} = "unknown VIFE " . sprintf("%x", $vifExtension) . " at offset " . ($offset-1);
- $dataBlockRef->{errorcode} = ERR_UNKNOWN_VIFE;
- }
- }
- $dataBlockRef->{VIFExtensions} = \@VIFExtensions;
-
- if ($dataBlockRef->{type} eq '') {
- $dataBlockRef->{type} = 'unknown';
- $dataBlockRef->{errormsg} = sprintf("in VIFExtension %x unknown VIF %x",$vifExtension, $vif);
- $dataBlockRef->{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;
-
- $dataBlockRef->{dif} = $dif;
-
- #printf("dif %02x 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) {
- $dataBlockRef->{errormsg} = 'too many DIFE';
- $dataBlockRef->{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);
- #printf("dife %x extno %d storage %d\n", $dif, $difExtNo, $storageNo);
- }
-
- $dataBlockRef->{functionField} = $functionField;
- $dataBlockRef->{functionFieldText} = $functionFieldTypes{$functionField};
- $dataBlockRef->{dataField} = $df;
- $dataBlockRef->{storageNo} = $storageNo;
- $dataBlockRef->{tariff} = $tariff;
- $dataBlockRef->{devUnit} = $devUnit;
-
- #printf("in DIF: datafield %x\n", $dataBlockRef->{dataField});
- #print "offset in dif $offset\n";
- 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;
- $dataBlock->{unit} = '';
-
- 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);
-
- # DIF_INT are _signed_ values
-
- if ($dataBlock->{dataField} == DIF_NONE or $dataBlock->{dataField} == DIF_READOUT) {
- $dataBlockNo--;
- $offset++;
- } 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_BCD12) {
- $value = $self->decodeBCD(12, substr($payload,$offset,6));
- $offset += 6;
- } elsif ($dataBlock->{dataField} == DIF_INT8) {
- $value = unpack('c', substr($payload, $offset, 1));
- $offset += 1;
- } elsif ($dataBlock->{dataField} == DIF_INT16) {
- $value = unpack('s<', substr($payload, $offset, 2));
- $offset += 2;
- } elsif ($dataBlock->{dataField} == DIF_INT24) {
- my @bytes = unpack('CCC', substr($payload, $offset, 3));
- $offset += 3;
- $value = $bytes[0] + $bytes[1] << 8 + $bytes[2] << 16;
- # two's complement
- $value = ~$value + 1;
- } elsif ($dataBlock->{dataField} == DIF_INT32) {
- $value = unpack('l<', substr($payload, $offset, 4));
- $offset += 4;
- } elsif ($dataBlock->{dataField} == DIF_INT48) {
- my @words = unpack('vvv', substr($payload, $offset, 6));
- $value = $words[0] + ($words[1] << 16) + ($words[2] << 32);
- # two's complement
- $value = ~$value + 1;
- $offset += 6;
- } elsif ($dataBlock->{dataField} == DIF_INT64) {
- $value = unpack('q<', substr($payload, $offset, 8));
- $offset += 8;
- } elsif ($dataBlock->{dataField} == DIF_FLOAT32) {
- #not allowed according to wmbus standard, Qundis seems to use it nevertheless
- $value = unpack('f', substr($payload, $offset, 4));
- $offset += 4;
- } elsif ($dataBlock->{dataField} == DIF_VARLEN) {
- my $lvar = unpack('C',substr($payload, $offset++, 1));
- #print "in datablock $dataBlockNo: LVAR field " . sprintf("%x", $lvar) . "\n";
- #printf "payload len %d offset %d\n", length($payload), $offset;
- if ($lvar <= 0xbf) {
- if ($dataBlock->{type} eq "MANUFACTURER SPECIFIC") {
- # special handling, LSE seems to lie about this
- $value = unpack('H*',substr($payload, $offset, $lvar));
- #print "VALUE: " . $value . "\n";
- } else {
- # ASCII string with LVAR characters
- $value = valueCalcAscii(substr($payload, $offset, $lvar), $dataBlock);
-
- if ($self->{manufacturer} eq 'ESY') {
- # Easymeter stores the string backwards!
- $value = reverse($value);
- }
- }
- $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
- #print "DIF_SPECIAL at $offset\n";
- $value = unpack("H*", substr($payload,$offset));
- 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";
- } elsif (defined $value) {
- $dataBlock->{value} = $value;
- } else {
- $dataBlock->{value} = "";
- }
-
- my $VIFExtensions = $dataBlock->{VIFExtensions};
- for my $VIFExtension (@$VIFExtensions) {
- $dataBlock->{extension_unit} = $VIFExtension->{unit};
- #printf("extension unit %s\n", $dataBlock->{extension_unit});
- if (defined $VIFExtension->{calcFunc}) {
- #printf("Extension value %d, valueFactor %d\n", $VIFExtension->{value}, $VIFExtension->{valueFactor});
- $dataBlock->{extension_value} = $VIFExtension->{calcFunc}->($VIFExtension->{value}, $dataBlock);
- } elsif (defined $VIFExtension->{value}) {
- $dataBlock->{extension_value} = sprintf("%x",$VIFExtension->{value});
- } else {
- #$dataBlock->{extension} = "";
- }
- }
- undef $value;
-
- push @dataBlocks, $dataBlock;
- }
-
- $self->{datablocks} = \@dataBlocks;
- return 1;
-}
-
-sub decrypt_mode5($) {
- my $self = shift;
- my $encrypted = shift;
- my $padding = 2;
-
- # see 4.2.5.3, page 26
- my $initVector = substr($self->{msg},2,8);
- for (1..8) {
- $initVector .= pack('C',$self->{access_no});
- }
-
- if (length($encrypted)%16 == 0) {
- # no padding if data length is multiple of blocksize
- $padding = 0;
- } else {
- $padding = 2;
- }
- #printf("length encrypted %d padding %d\n", length($encrypted), $padding);
- my $cipher = Crypt::Mode::CBC->new('AES', $padding);
- return $cipher->decrypt($encrypted, $self->{aeskey}, $initVector);
-}
-
-sub decrypt_mode7($) {
- my $self = shift;
- my $encrypted = shift;
- my $padding = 2;
- my $identno;
-
- # generate dynamic key
- my $cmac = Digest::CMAC->new($self->{aeskey});
- #my $cmac = Digest::CMAC->new(pack("H*",'000102030405060708090A0B0C0D0E0F'));
-
- # The calculation of Kenc and Kmac for the meter:
- # Kenc = CMAC(MK, 0x00 ||C[7..0] ||C[15..8] ||C[23..16] ||C[31..24] ||ID_0||ID_1||ID_2||ID_3||0x07||0x07||0x07||0x07||0x07||0x07||0x07)
- # Where C[7..0] is the LSB and C[31..24] is the MSB (Big Endian) of the counter AFL.MCR.C from meter to other (gateway).
-
- $cmac->add(pack("H*", "00"));
-
- #$self->{afl}{mcr} = pack("H*", "b30a0000");
- $cmac->add($self->{afl}{mcr});
- #print "MCR " . unpack("H*", $self->{afl}{mcr}) . "\n";
- if (exists($self->{meter_id_raw})) {
- $identno = $self->{meter_id_raw};
- } else {
- $identno = $self->{afield_identno};
- }
- #print "identno " . unpack("H*", $identno) . "\n";
- $cmac->add($identno);
- $cmac->add(pack("H*", "07070707070707"));
- #$cmac->add(pack("H*",'7856341207070707070707'));
-
-
-
- my $key = $cmac->digest;
-
- #printf("Dynamic key %s\n", $cmac->hexdigest);
-
- # see 9.2.4, page 59
- my $initVector = '';
- for (1..16) {
- $initVector .= pack('C',0x00);
- }
- if (length($encrypted)%16 == 0) {
- # no padding if data length is multiple of blocksize
- $padding = 0;
- } else {
- $padding = 2;
- }
-
- #$encrypted = pack("H*","9058475F4BC91DF878B80A1B0F98B629024AAC727942BFC549233C0140829B93");
- #print unpack("H*", $encrypted) . "\n";
- my $cipher = Crypt::Mode::CBC->new('AES', $padding);
- return $cipher->decrypt($encrypted, $key, $initVector);
-}
-
-sub decodeAFL($$) {
- my $self = shift;
- my $afl = shift;
- my $offset = 0;
-
-
- $self->{afl}{fcl} = unpack('v', $afl);
- $offset += 2;
- $self->{afl}{fcl_mf} = ($self->{afl}{fcl} & 0b0100000000000000) != 0;
- $self->{afl}{fcl_mclp} = ($self->{afl}{fcl} & 0b0010000000000000) != 0;
- $self->{afl}{fcl_mlp} = ($self->{afl}{fcl} & 0b0001000000000000) != 0;
- $self->{afl}{fcl_mcrp} = ($self->{afl}{fcl} & 0b0000100000000000) != 0;
- $self->{afl}{fcl_macp} = ($self->{afl}{fcl} & 0b0000010000000000) != 0;
- $self->{afl}{fcl_kip} = ($self->{afl}{fcl} & 0b0000001000000000) != 0;
- $self->{afl}{fcl_fid} = $self->{afl}{fcl} & 0b0000000011111111;
-
- if ($self->{afl}{fcl_mclp}) {
- # AFL Message Control Field (AFL.MCL)
- $self->{afl}{mcl} = unpack('C', substr($afl, $offset, 1));
- #printf "AFL MCL %01x\n", $self->{afl}{mcl};
- $offset += 1;
- $self->{afl}{mcl_mlmp} = ($self->{afl}{mcl} & 0b01000000) != 0;
- $self->{afl}{mcl_mcmp} = ($self->{afl}{mcl} & 0b00100000) != 0;
- $self->{afl}{mcl_kimp} = ($self->{afl}{mcl} & 0b00010000) != 0;
- $self->{afl}{mcl_at} = ($self->{afl}{mcl} & 0b00001100) >> 2;
- $self->{afl}{mcl_ato} = ($self->{afl}{mcl} & 0b00000011);
- }
- if ($self->{afl}{fcl_mcrp}) {
- # AFL Message Counter Field (AFL.MCR)
- #$self->{afl}{mcr} = unpack('N', substr($afl, $offset));
- $self->{afl}{mcr} = substr($afl, $offset, 4);
- $offset += 4;
- }
- if ($self->{afl}{fcl_mlp}) {
- # AFL Message Length Field (AFL.ML)
- $self->{afl}{ml} = unpack('v', substr($afl, $offset));
- $offset += 2;
- }
- if ($self->{afl}{mcl_at} == 1) {
- # CMAC-AES128 (see 9.3.1)
- my $mac_len = 0;
- if ($self->{afl}{mcl_ato} == 1) {
- $mac_len = 8;
- $self->{afl}{mac} = (unpack('N', substr($afl, $offset, 4))) << 32 | ((unpack('N', substr($afl, $offset+4, 4))));
- #printf "AFL MAC %8x\n", $self->{afl}{mac};
- } else {
- # reserved
- }
- $offset += $mac_len;
- }
- if ($self->{afl}{fcl_kip}) {
- # AFL Key Information-Field (AFL.KI)
- $self->{afl}{ki} = unpack('v', $afl);
- $self->{afl}{ki_key_version} = ($self->{afl}{ki} & 0b1111111100000000) >> 8;
- $self->{afl}{ki_kdf_selection} = ($self->{afl}{ki} & 0b0000000001110000) >> 4;
- $self->{afl}{ki_key_id} = ($self->{afl}{ki} & 0b0000000000001111);
- $offset += 2;
- }
- return $offset;
-}
-
-sub decodeCompactFrame($$)
-{
- my $self = shift;
- my $compact = shift;
- my $applicationlayer = "";
-
- # VIF depends on the resolution of the volume register
- # 13 = 3 decimals
- # 14 = 2 decimals
- # 15 = 1 decimal
- # 16 = 0 decimals
- # functionField part of DIF is also variable, at least for temperatures
- # all in all that would be 4 * 4 (for vif) * 4 * 4 (for dif) * 3 (type of telegram) combinations (768)
- # for now only search for those that are documented or habe been observed in real telegrams
- for my $vif ("13","14","15","16") {
- #printf("compact frame $vif\n");
- if ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif"))) {
- # Info, Volume, Target Volume
- # convert into full frame
- $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
- . pack("H*", "04$vif") . substr($compact,7,4) # volume
- . pack("H*", "44$vif") . substr($compact,11,4); # target volume
- last;
- } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "523B"))) {
- # Info, Volume, Max flow
- # convert into full frame
- $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
- . pack("H*", "04$vif") . substr($compact,7,4) # volume
- . pack("H*", "523B") . substr($compact,11,2); # max flow
- last;
- } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif" . "615B" . "6167"))) {
- # Info, Volume, Max flow, min flow temp, max external temp
- # convert into full frame
- $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
- . pack("H*", "04$vif") . substr($compact,7,4) # volume
- . pack("H*", "44$vif") . substr($compact,11,4) # target volume
- . pack("H*", "615B") . substr($compact,15,1) # flow temp
- . pack("H*", "6167") . substr($compact,16,1); # external temp
- last;
- } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif" . "615B" . "5167"))) {
- # Info, Volume, Max flow, min flow temp, max external temp
- # convert into full frame
- $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
- . pack("H*", "04$vif") . substr($compact,7,4) # volume
- . pack("H*", "44$vif") . substr($compact,11,4) # target volume
- . pack("H*", "615B") . substr($compact,15,1) # flow temp
- . pack("H*", "5167") . substr($compact,16,1); # external temp
- last;
- }
- }
- return $applicationlayer;
-}
-
-sub decodeApplicationLayer($) {
- my $self = shift;
- my $applicationlayer = $self->{applicationlayer};
- my $payload;
-
- #print unpack("H*", $applicationlayer) . "\n";
-
- $self->{isEncrypted} = 0;
- if ($self->{errorcode} != ERR_NO_ERROR) {
- # CRC check failed
- return 0;
- }
- $self->{cifield} = unpack('C', $applicationlayer);
-
- my $offset = 1;
- my $has_ell = 1;
-
- if ($self->{cifield} == CI_ELL_2) {
- # Extended Link Layer
- ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset));
- $offset += 2;
- } elsif ($self->{cifield} == CI_ELL_10) {
- # Extended Link Layer (long)
- ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset));
- $offset += 2;
- $self->{ell}{manufacturer} = substr($applicationlayer,$offset, 2);
- $offset += 2;
- $self->{ell}{identno} = substr($applicationlayer,$offset, 4);
- $offset += 4;
- ($self->{ell}{version},$self->{ell}{device}) = unpack('CC', substr($applicationlayer,$offset));
- $offset += 2;
- } elsif ($self->{cifield} == CI_ELL_8) {
- # Extended Link Layer, payload CRC is part of (encrypted) payload
- ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{session_number}) = unpack('CCV', substr($applicationlayer, $offset));
- $offset += 6;
- } elsif ($self->{cifield} == CI_ELL_16) {
- # Extended Link Layer
- ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{m2}, $self->{ell}{a2}, $self->{ell}{session_number}) = unpack('CCvC6V', substr($applicationlayer,$offset));
- $offset += 14;
- } else {
- $has_ell = 0;
- }
-
- if (exists($self->{ell}{session_number})) {
- $self->{ell}{session_number_enc} = $self->{ell}{session_number} >> 29;
- $self->{ell}{session_number_time} = ($self->{ell}{session_number} & 0b0001111111111111111111111111111) >> 4;
- $self->{ell}{session_number_session} = $self->{ell}{session_number} & 0b1111;
- $self->{isEncrypted} = $self->{ell}{session_number_enc} != 0;
- $self->{decrypted} = 0;
-
- if ($self->{isEncrypted}) {
- if ($self->{aeskey}) {
- if ($hasCTR) {
- # AES IV
- # M-field, A-field, CC, SN, 00, 0000
- my $initVector = pack("v", $self->{mfield}) . $self->{afield} . pack("CV", $self->{ell}{cc}, $self->{ell}{session_number}) . pack("H*", "000000");
- my $m = Crypt::Mode::CTR->new('AES', 1);
- my $ciphertext = substr($applicationlayer,$offset); # payload CRC must also be decrypted
- #printf("##ciphertext: %s\n", unpack("H*", $ciphertext));
- $payload = $m->decrypt($ciphertext, $self->{aeskey}, $initVector);
-
- #printf("##plaintext %s\n", unpack("H*", $payload));
- } else {
- $self->{errormsg} = 'Crypt::Mode::CTR is not installed, please install it (sudo cpan -i Crypt::Mode::CTR)';
- $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
- return 0;
- }
- } else {
- $self->{errormsg} = 'encrypted message and no aeskey provided';
- $self->{errorcode} = ERR_NO_AESKEY;
- return 0;
- }
- }
- $self->{ell}{crc} = unpack('v', $payload);
- $offset += 2;
- # PayloadCRC is a cyclic redundancy check covering the remainder of the frame (excluding the CRC fields)
- # payload CRC is also encrypted
- if ($self->{ell}{crc} != $self->calcCRC(substr($payload, 2, $self->{lfield}-20))) {
- #printf("crc %x, calculated %x\n", $self->{ell}{crc}, $self->calcCRC(substr($payload, 2, $self->{lfield}-20)));
- $self->{errormsg} = "Payload CRC check failed on ELL" . ($self->{isEncrypted} ? ", wrong AES key?" : "");
- $self->{errorcode} = ERR_CRC_FAILED;
- return 0;
- } else {
- $self->{decrypted} = 1;
- }
- $applicationlayer = $payload;
- $offset = 2; # skip PayloadCRC
- }
-
- if ($offset > 1) {
- $applicationlayer = substr($applicationlayer,$offset);
- $self->{cifield} = unpack('C', $applicationlayer);
- $offset = 1;
- if ($self->{cifield} == CI_AFL) {
- # Authentification and Fragmentation Layer
- $self->{afl}{afll} = unpack('C', substr($applicationlayer, $offset));
- #printf "AFL AFLL %02x\n", $self->{afl}{afll};
- $offset += 1;
- $self->decodeAFL(substr($applicationlayer,$offset,$self->{afl}{afll}));
- $offset += $self->{afl}{afll};
- if ($self->{afl}{fcl_mf}) {
- $self->{errormsg} = "fragmented messages are not yet supported";
- $self->{errorcode} = ERR_FRAGMENT_UNSUPPORTED;
- return 0;
- }
- }
- }
-
- if ($offset > 1) {
- $applicationlayer = substr($applicationlayer,$offset);
- $self->{cifield} = unpack('C', $applicationlayer);
- $offset = 1;
- }
-
- # initialize some fields
- $self->{cw_1} = 0;
- $self->{cw_2} = 0;
- $self->{cw_3} = 0;
- $self->{status} = 0;
- $self->{statusstring} = "";
- $self->{access_no} = 0;
- $self->{sent_from_master} = 0;
-
- #printf("CI Field %02x\n", $self->{cifield});
-
- # Config Word ist normalerweise 2 Bytes lang, nur bei encryption mode 7 drei Bytes
- # erstmal drei Bytes auslesen, aber den Offset nur um 2 Bytes erhöhen
-
- if ($self->{cifield} == CI_RESP_4 || $self->{cifield} == CI_RESP_SML_4) {
- # Short header
- ($self->{access_no}, $self->{status}, $self->{cw_1}, $self->{cw_2}, $self->{cw_3}) = unpack('CCCCC', substr($applicationlayer,$offset));
- #printf("Short header access_no %x\n", $self->{access_no});
- $offset += 4;
- } elsif ($self->{cifield} == CI_RESP_12 || $self->{cifield} == CI_RESP_SML_12) {
- # Long header
- $self->{meter_id_raw} = substr($applicationlayer,$offset,4);
- ($self->{meter_man}, $self->{meter_vers}, $self->{meter_dev}, $self->{access_no}, $self->{status}, $self->{cw_1}, $self->{cw_2}, $self->{cw_3})
- = unpack('vCCCCCCC', substr($applicationlayer,$offset+4));
- $self->{meter_id} = sprintf("%08d", unpack('V', $self->{meter_id_raw}));
- $self->{meter_devtypestring} = $validDeviceTypes{$self->{meter_dev}} || 'unknown';
- $self->{meter_manufacturer} = uc($self->manId2ascii($self->{meter_man}));
- #printf("Long header access_no %x\n", $self->{access_no});
- $offset += 12;
- } elsif ($self->{cifield} == CI_RESP_0 || $self->{cifield} == 0x30) {
- # no header
- #print "No header\n";
-
- } elsif ($self->{cifield} == 0x79 && $self->{manufacturer} eq 'KAM') {
- #print "Kamstrup compact frame header\n";
- $self->{format_signature} = unpack("v", substr($applicationlayer,$offset, 2));
- $offset += 2;
- $self->{full_frame_payload_crc} = unpack("v", substr($applicationlayer, $offset, 2));
- $offset += 2;
- $applicationlayer = $self->decodeCompactFrame($applicationlayer);
- if ($applicationlayer eq "") {
- $self->{errormsg} = 'Unknown Kamstrup compact frame format';
- $self->{errorcode} = ERR_UNKNOWN_COMPACT_FORMAT;
- return 0;
- } else {
- $offset = 0;
- }
- if ($self->{full_frame_payload_crc} != $self->calcCRC($applicationlayer)) {
- $self->{errormsg} = 'Kamstrup compact frame format payload CRC error';
- $self->{errorcode} = ERR_CRC_FAILED;
- return 0;
- }
- } elsif ($self->{cifield} == CI_SND_UD_MODE_1 || $self->{cifield} == CI_SND_UD_MODE_2) {
- $self->{sent_from_master} = 1;
- # The EN1434-3 defines two possible data sequences in multibyte records.
- # The bit two (counting begins with bit 0, value 4), which is called M bit or Mode bit,
- # in the CI field gives an information about the used byte sequence in multibyte data structures.
- # If the Mode bit is not set (Mode 1), the least significant byte of a multibyte record is transmitted first,
- # otherwise (Mode 2) the most significant byte.
- # The Usergroup recommends to use only the Mode 1 in future applications.
- $self->{mode_bit} = $self->{cifield} & 4;
- } else {
- # unsupported
- $self->decodeConfigword();
- $self->{errormsg} = 'Unsupported CI Field ' . sprintf("%x", $self->{cifield}) . ", remaining payload is " . unpack("H*", substr($applicationlayer,$offset));
- $self->{errorcode} = ERR_UNKNOWN_CIFIELD;
- return 0;
- }
- $self->{statusstring} = join(", ", $self->state2string($self->{status}));
-
- $self->decodeConfigword();
-
- $self->{encryptionMode} = $encryptionModes{$self->{cw_parts}{mode}};
- if ($self->{cw_parts}{mode} == 0) {
- # no encryption
- if (!$self->{isEncrypted}) {
- $self->{decrypted} = 1;
- }
- $payload = substr($applicationlayer, $offset);
- } elsif ($self->{cw_parts}{mode} == 5 || $self->{cw_parts}{mode} == 7) {
- # data is encrypted with AES 128, dynamic init vector
- # decrypt data before further processing
- $self->{isEncrypted} = 1;
- $self->{decrypted} = 0;
-
- if ($self->{aeskey}) {
- if ($hasCBC) {
- # payload can be only partially encrypted.
- # decrypt only the encrypted part
- my $encrypted_length = $self->{cw_parts}{encrypted_blocks} * 16;
- if ($self->{cw_parts}{mode} == 5) {
- #printf("encrypted payload %s\n", unpack("H*", substr($applicationlayer,$offset, $encrypted_length)));
- eval {
- $payload = $self->decrypt_mode5(substr($applicationlayer, $offset, $encrypted_length));
- };
- } else {
- # mode 7
- if ($hasCMAC) {
- $offset++; # account for codeword byte 3
- #printf("encrypted payload %s\n", unpack("H*", substr($applicationlayer,$offset, $encrypted_length)));
- eval {
- $payload = $self->decrypt_mode7(substr($applicationlayer, $offset, $encrypted_length));
- }
- } else {
- $self->{errormsg} = 'Digest::CMAC is not installed, please install it (sudo cpan -i Digest::CMAC)';
- $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
- return 0;
- }
- }
- if ($@) {
- #fatal decryption error occurred
- $self->{errormsg} = "fatal decryption error for mode " . $self->{cw_parts}{mode} . ": $@";
- $self->{errorcode} = ERR_DECRYPTION_FAILED;
- return 0;
- }
- # add unencrypted payload
- $payload .= substr($applicationlayer, $offset+$encrypted_length);
- #printf("decrypted payload %s\n", unpack("H*", $payload));
- if (unpack('n', $payload) == 0x2f2f) {
- $self->{decrypted} = 1;
- } else {
- # Decryption verification failed
- $self->{errormsg} = sprintf('Decryption mode %d failed, wrong key?', $self->{cw_parts}{mode});
- $self->{errorcode} = ERR_DECRYPTION_FAILED;
- #printf("%x\n", unpack('n', $payload));
- return 0;
- }
- } else {
- $self->{errormsg} = 'Crypt::Mode::CBC is not installed, please install it (sudo cpan -i Crypt::Mode::CBC)';
- $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
- return 0;
- }
- } else {
- $self->{errormsg} = 'encrypted message and no aeskey provided';
- $self->{errorcode} = ERR_NO_AESKEY;
- return 0;
- }
- } else {
- # error, encryption mode not implemented
- $self->{errormsg} = sprintf('Encryption mode %x not implemented', $self->{cw_parts}{mode});
- $self->{errorcode} = ERR_UNKNOWN_ENCRYPTION;
- $self->{isEncrypted} = 1;
- $self->{decrypted} = 0;
- return 0;
- }
-
- if ($self->{cifield} == CI_RESP_SML_4 || $self->{cifield} == CI_RESP_SML_12) {
- # payload is SML encoded, that's not implemented
- $self->{errormsg} = "payload is SML encoded, can't be decoded, SML payload is " . unpack("H*", substr($applicationlayer,$offset));
- $self->{errorcode} = ERR_SML_PAYLOAD;
- return 0;
- } else {
- return $self->decodePayload($payload);
- }
-
-}
-
-sub decodeLinkLayer($$)
-{
- my $self = shift;
- my $linklayer = shift;
- #print "decodeLinkLayer\n";
-
- if (length($linklayer) < TL_BLOCK_SIZE + $self->{crc_size}) {
- $self->{errormsg} = "link layer too short";
- $self->{errorcode} = ERR_LINK_LAYER_INVALID;
- return 0;
- }
- ($self->{lfield}, $self->{cfield}, $self->{mfield}) = unpack('CCv', $linklayer);
- $self->{afield} = substr($linklayer,4,6);
- $self->{afield_identno} = substr($self->{afield}, 0, 4);
- $self->{afield_id} = sprintf("%08d", $self->decodeBCD(8,substr($linklayer,4,4)));
- ($self->{afield_ver}, $self->{afield_type}) = unpack('CC', substr($linklayer,8,2));
-
- #printf("lfield %d\n", $self->{lfield});
-
- if ($self->{frame_type} eq FRAME_TYPE_A) {
- #print "FRAME TYPE A\n";
- if ($self->{crc_size} > 0) {
- $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size}));
-
- #printf("crc0 %x calc %x\n", $self->{crc0}, $self->calcCRC(substr($linklayer,0,10)));
-
- if ($self->{crc0} != $self->calcCRC(substr($linklayer,0,TL_BLOCK_SIZE))) {
- $self->{errormsg} = "CRC check failed on link layer";
- $self->{errorcode} = ERR_CRC_FAILED;
- #print "CRC check failed on link layer\n";
- return 0;
- }
- }
-
- # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller
- $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself
- $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE);
- $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0;
- $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size};
-
- #printf("calc len %d, actual %d crc_size %d\n", $self->{msglen}, length($self->{msg}), $self->{crc_size});
- $self->{applicationlayer} = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size}));
- return 0 if $self->{errorcode};
-
- } else {
- # FRAME TYPE B
- # each block is at most 129 bytes long.
- # first contains the header (TL_BLOCK), L field and trailing crc
- # L field is included in crc calculation
- # each following block contains only data and trailing crc
- #print "FRAME TYPE B\n";
- if (length($self->{msg}) < $self->{lfield}) {
- $self->{errormsg} = "message too short, expected " . $self->{lfield} . ", got " . length($self->{msg}) . " bytes";
- $self->{errorcode} = ERR_MSG_TOO_SHORT;
- return 0;
- }
-
-
- my $length = 129;
- if ($self->{lfield} < $length) {
- $length = $self->{lfield};
- }
- if ($self->{crc_size} > 0) {
- $length -= $self->{crc_size};
- $length++; # for L field
- #print "length: $length\n";
- $self->{crc0} = unpack('n', substr($self->{msg}, $length, $self->{crc_size}));
-
- #printf "crc in msg %x crc calculated %x\n", $self->{crc0}, $self->calcCRC(substr($self->{msg}, 0, $length));
- if ($self->{crc0} != $self->calcCRC(substr($self->{msg}, 0, $length))) {
- $self->{errormsg} = "CRC check failed on block 1";
- $self->{errorcode} = ERR_CRC_FAILED;
- return 0;
- }
- }
-
- $self->{datablocks} = int($self->{lfield} / 129);
- $self->{datablocks}++ if $self->{lfield} % 129 != 0;
- # header block is 10 bytes, following block
- $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1) - ($self->{datablocks} * $self->{crc_size}) ; # this is with CRCs but without the lfield itself
- $self->{msglen} = $self->{lfield};
-
- if ($self->{datablocks} == 2) {
- # TODO
- } else {
- $self->{applicationlayer} = substr($self->{msg}, TL_BLOCK_SIZE, $length - TL_BLOCK_SIZE); # - $self->{crc_size});
- }
- }
-
- if (length($self->{msg}) > $self->{msglen}) {
- $self->{remainingData} = substr($self->{msg},$self->{msglen});
- } elsif (length($self->{msg}) < $self->{msglen}) {
- $self->{errormsg} = "message too short, expected " . $self->{msglen} . ", got " . length($self->{msg}) . " bytes";
- $self->{errorcode} = ERR_MSG_TOO_SHORT;
- return 0;
- }
-
-
- # according to the MBus spec only upper case letters are allowed.
- # some devices send lower case letters none the less
- # convert to upper case to make them spec conformant
- $self->{manufacturer} = uc($self->manId2ascii($self->{mfield}));
- $self->{typestring} = $validDeviceTypes{$self->{afield_type}} || 'unknown';
- return 1;
-}
-
-sub encodeLinkLayer($)
-{
- my $self = shift;
-
- my $linklayer = pack('CCv', $self->{lfield}, $self->{cfield}, $self->{mfield});
- ($self->{lfield}, $self->{cfield}, $self->{mfield}) = unpack('CCv', $linklayer);
- $self->{afield} = substr($linklayer,4,6);
- $self->{afield_id} = sprintf("%08d", $self->decodeBCD(8,substr($linklayer,4,4)));
- ($self->{afield_ver}, $self->{afield_type}) = unpack('CC', substr($linklayer,8,2));
-
- #printf("lfield %d\n", $self->{lfield});
-
- if ($self->{frame_type} eq FRAME_TYPE_A) {
- if ($self->{crc_size} > 0) {
- $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size}));
-
- #printf("crc0 %x calc %x\n", $self->{crc0}, $self->calcCRC(substr($linklayer,0,10)));
-
- if ($self->{crc0} != $self->calcCRC(substr($linklayer,0,TL_BLOCK_SIZE))) {
- $self->{errormsg} = "CRC check failed on link layer";
- $self->{errorcode} = ERR_CRC_FAILED;
- #print "CRC check failed on link layer\n";
- return 0;
- }
- }
-
- # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller
- $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself
- $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE);
- $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0;
- $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size};
-
- #printf("calc len %d, actual %d\n", $self->{msglen}, length($self->{msg}));
- $self->{applicationlayer} = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size}));
-
- } else {
- # FRAME TYPE B
- # each block is at most 129 bytes long.
- # first contains the header (TL_BLOCK), L field and trailing crc
- # L field is included in crc calculation
- # each following block contains only data and trailing crc
- if (length($self->{msg}) < $self->{lfield}) {
- $self->{errormsg} = "message too short, expected " . $self->{lfield} . ", got " . length($self->{msg}) . " bytes";
- $self->{errorcode} = ERR_MSG_TOO_SHORT;
- return 0;
- }
-
-
- my $length = 129;
- if ($self->{lfield} < $length) {
- $length = $self->{lfield};
- }
- if ($self->{crc_size} > 0) {
- $length -= $self->{crc_size};
- $length++; # for L field
- #print "length: $length\n";
- $self->{crc0} = unpack('n', substr($self->{msg}, $length, $self->{crc_size}));
-
- #printf "crc in msg %x crc calculated %x\n", $self->{crc0}, $self->calcCRC(substr($self->{msg}, 0, $length));
- if ($self->{crc0} != $self->calcCRC(substr($self->{msg}, 0, $length))) {
- $self->{errormsg} = "CRC check failed on block 1";
- $self->{errorcode} = ERR_CRC_FAILED;
- return 0;
- }
- }
-
- $self->{datablocks} = int($self->{lfield} / 129);
- $self->{datablocks}++ if $self->{lfield} % 129 != 0;
- # header block is 10 bytes, following block
- $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1) - ($self->{datablocks} * $self->{crc_size}) ; # this is with CRCs but without the lfield itself
- $self->{msglen} = $self->{lfield};
-
- if ($self->{datablocks} == 2) {
- # TODO
- } else {
- $self->{applicationlayer} = substr($self->{msg}, TL_BLOCK_SIZE, $length - TL_BLOCK_SIZE); # - $self->{crc_size});
- }
- }
-
- if (length($self->{msg}) > $self->{msglen}) {
- $self->{remainingData} = substr($self->{msg},$self->{msglen});
- } elsif (length($self->{msg}) < $self->{msglen}) {
- $self->{errormsg} = "message too short, expected " . $self->{msglen} . ", got " . length($self->{msg}) . " bytes";
- $self->{errorcode} = ERR_MSG_TOO_SHORT;
- return 0;
- }
-
-
- # according to the MBus spec only upper case letters are allowed.
- # some devices send lower case letters none the less
- # convert to upper case to make them spec conformant
- $self->{manufacturer} = uc($self->manId2ascii($self->{mfield}));
- $self->{typestring} = $validDeviceTypes{$self->{afield_type}} || 'unknown';
- return 1;
-}
-
-
-sub setFrameType($$)
-{
- my $self = shift;
- $self->{frame_type} = shift;
-}
-
-sub getFrameType($)
-{
- my $self = shift;
- return $self->{frame_type};
-}
-
-sub parse($$)
-{
- my $self = shift;
- $self->{msg} = shift;
-
- $self->{errormsg} = '';
- $self->{errorcode} = ERR_NO_ERROR;
-
- if (length($self->{msg}) < 12) {
- $self->{errormsg} = "Message too short";
- $self->{errorcode} = ERR_MSG_TOO_SHORT;
- return 1;
- }
- if (substr($self->{msg}, 0, 4) eq pack("H*", "543D543D")) {
- $self->setFrameType(FRAME_TYPE_B);
- $self->{msg} = substr($self->{msg},4);
- }
- 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();
-}
-
-sub dumpResult($)
-{
- my $self = shift;
-
- if ($self->{linkLayerOk}) {
- printf("Manufacturer %x %s\n", $self->{mfield}, $self->{manufacturer});
- printf("IdentNumber %s\n", $self->{afield_id});
- printf("Version %d\n", $self->{afield_ver});
- printf("Type %x %s\n", $self->{afield_type}, $self->{typestring});
- printf("IsEncrypted %d\n", $self->{isEncrypted});
-
- printf("Status: %x %s\n", $self->{status}, $self->{statusstring});
- if ($self->{cw_parts}{mode} == 5) {
- print "Codeword:\n";
- print "bidirectional: ". $self->{cw_parts}{bidirectional} . "\n";
- print "accessability: ". $self->{cw_parts}{accessability} . "\n";
- print "synchronous: $self->{cw_parts}{synchronous}\n";
- print "mode: $self->{cw_parts}{mode}\n";
- print "encrypted_blocks: $self->{cw_parts}{encrypted_blocks}\n";
- print "content: $self->{cw_parts}{content}\n";
- print "hops: $self->{cw_parts}{hops}\n";
- }
- }
-
- if ($self->{errorcode} == ERR_NO_ERROR) {
- if ($self->{cifield} == CI_RESP_12) {
- printf("Meter Id %d\n", $self->{meter_id});
- printf("Meter Manufacturer %x %s\n", $self->{meter_man}, $self->manId2ascii($self->{meter_man}));
- printf("Meter Version %d\n", $self->{meter_vers});
- printf("Meter Dev %x %s\n", $self->{meter_dev}, $self->type2string($self->{meter_dev}));
- printf("Access No %d\n", $self->{access_no});
- printf("Status %x\n", $self->{status});
- }
-
- my $dataBlocks = $self->{datablocks};
- my $dataBlock;
-
- for $dataBlock ( @$dataBlocks ) {
- #if ( $dataBlock->{type} eq "MANUFACTURER SPECIFIC") {
- # print $dataBlock->{number} . " " . $dataBlock->{type} . "\n";
- #} else {
- print $dataBlock->{number} . ". StorageNo " . $dataBlock->{storageNo} . " " ;
- print $dataBlock->{functionFieldText} . " ";
- print $dataBlock->{type} . " " . $dataBlock->{value} . " " . $dataBlock->{unit};
- if ($dataBlock->{errormsg}) {
- print "(" . $dataBlock->{errormsg} . ")";
- }
- if (defined($dataBlock->{extension_unit})) {
- print " [" . $dataBlock->{extension_unit} . ", " . $dataBlock->{extension_value} . "]";
- }
- print "\n";
- #}
- }
- } else {
- printf("Error %d: %s\n", $self->{errorcode}, $self->{errormsg});
- }
-}
-
-1;
+# $Id$
+
+package WMBus;
+
+use strict;
+use warnings;
+use feature qw(say);
+use Scalar::Util qw(looks_like_number);
+use Digest::CRC; # libdigest-crc-perl
+eval "use Crypt::Mode::CBC"; # cpan -i Crypt::Mode::CBC
+my $hasCBC = ($@)?0:1;
+eval "use Crypt::Mode::CTR"; # cpan -i Crypt::Mode::CTR
+my $hasCTR = ($@)?0:1;
+eval "use Digest::CMAC"; # cpan -i Digest::CMAC
+my $hasCMAC = ($@)?0:1;
+
+require Exporter;
+my @ISA = qw(Exporter);
+my @EXPORT = qw(new parse parseLinkLayer parseApplicationLayer manId2ascii type2string setFrameType getFrameType VIF_TYPE_MANUFACTURER_SPECIFIC);
+
+sub manId2ascii($$);
+
+
+use constant {
+ # Transport Layer block size
+ TL_BLOCK_SIZE => 10,
+ # Link Layer block size
+ LL_BLOCK_SIZE => 16,
+ # size of CRC in bytes
+ CRC_SIZE => 2,
+
+ # 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
+ CI_RESP_0 => 0x78, # Response from device, 0 Byte header, variable length
+ CI_ERROR => 0x70, # Error from device, only specified for wired M-Bus but used by Easymeter WMBUS module
+ CI_TL_4 => 0x8a, # Transport layer from device, 4 Bytes
+ CI_TL_12 => 0x8b, # Transport layer from device, 12 Bytes
+ CI_ELL_2 => 0x8c, # Extended Link Layer, 2 Bytes
+ CI_ELL_10 => 0x8e, # Extended Link Layer, 10 Bytes
+ CI_ELL_8 => 0x8d, # Extended Link Layer, 8 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4)
+ CI_ELL_16 => 0x8f, # Extended Link Layer, 16 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4)
+ CI_AFL => 0x90, # Authentification and Fragmentation Layer, variable size
+ CI_RESP_SML_4 => 0x7e, # Response from device, 4 Bytes, application layer SML encoded
+ CI_RESP_SML_12 => 0x7f, # Response from device, 12 Bytes, application layer SML encoded
+ CI_SND_UD_MODE_1 => 0x51, # The master can send data to a slave using a SND_UD with CI-Field 51h for mode 1 or 55h for mode 2
+ CI_SND_UD_MODE_2 => 0x55,
+
+ # DIF types (Data Information Field), see page 32
+ DIF_NONE => 0x00,
+ DIF_INT8 => 0x01,
+ DIF_INT16 => 0x02,
+ DIF_INT24 => 0x03,
+ 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,
+ ERR_TOO_MANY_VIFE => 11,
+ ERR_MSG_TOO_SHORT => 12,
+ ERR_SML_PAYLOAD => 13,
+ ERR_FRAGMENT_UNSUPPORTED => 14,
+ ERR_UNKNOWN_COMPACT_FORMAT => 15,
+ ERR_CIPHER_NOT_INSTALLED => 16,
+ ERR_LINK_LAYER_INVALID => 17,
+
+ VIF_TYPE_MANUFACTURER_SPECIFIC => 'MANUFACTURER SPECIFIC',
+
+ # TYPE C transmission uses two different frame types
+ # see http://www.st.com/content/ccc/resource/technical/document/application_note/3f/fb/35/5a/25/4e/41/ba/DM00233038.pdf/files/DM00233038.pdf/jcr:content/translations/en.DM00233038.pdf
+ FRAME_TYPE_A => 'A',
+ FRAME_TYPE_B => 'B',
+
+ # content type (CC bits of configuration field)
+ # stored in $self->{cw_parts}{content}
+ CONTENT_STANDARD => 0b00, # Standard data message with unsigned variable meter data
+ CONTENT_STATIC => 0b10, # Static message (consists of parameter, OBIS definitions and other data points
+ # which are not frequently changed – see also 4.3.2.4).
+
+
+};
+
+sub valueCalcNumeric($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ # some sanity checks on the provided data
+ if (defined($value) && defined($dataBlock->{valueFactor}) && looks_like_number($value))
+ {
+ return $value * $dataBlock->{valueFactor};
+ } else {
+ return 0;
+ }
+
+}
+
+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 sprintf("invalid: %x", $value);
+ } else {
+ return sprintf("%04d-%02d-%02d", $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 $timeInvalid = $value & 0b10000000;
+
+ my $dateTime = valueCalcDate($datePart, $dataBlock);
+ if ($timeInvalid == 0) {
+ my $min = ($value & 0b111111);
+ my $hour = ($value >> 8) & 0b11111;
+ my $su = ($value & 0b1000000000000000);
+ if ($min > 59 || $hour > 23) {
+ $dateTime = sprintf('invalid: %x', $value);
+ } else {
+ $dateTime .= sprintf(' %02d:%02d %s', $hour, $min, $su ? 'DST' : '');
+ }
+ }
+
+ return $dateTime;
+}
+
+sub valueCalcHex($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ return unpack("H*", $value);
+}
+
+sub valueCalcAscii($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ my $result = unpack('a*',$value);
+
+ # replace non printable chars
+ $result =~ s/[\x00-\x1f\x7f-\xff]/?/g;
+
+ return $result;
+}
+
+sub valueCalcu($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ my $result = '';
+
+ $result = ($value & 0b00001000 ? 'upper' : 'lower') . ' limit';
+ return $result;
+}
+
+sub valueCalcufnn($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ my $result = '';
+
+ $result = ($value & 0b00001000 ? 'upper' : 'lower') . ' limit';
+ $result .= ', ' . ($value & 0b00000100 ? 'first' : 'last');
+ $result .= sprintf(', duration %d', $value & 0b11);
+ return $result;
+}
+
+sub valueCalcMultCorr1000($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ $dataBlock->{value} *= 1000;
+
+ return "correction by factor 1000";
+}
+
+
+my %TimeSpec = (
+ 0b00 => 's', # seconds
+ 0b01 => 'm', # minutes
+ 0b10 => 'h', # hours
+ 0b11 => 'd', # days
+);
+
+sub valueCalcTimeperiod($$) {
+ my $value = shift;
+ my $dataBlock = shift;
+
+ $dataBlock->{unit} = $TimeSpec{$dataBlock->{exponent}};
+ return $value;
+}
+
+# VIF types (Value Information Field), see page 32
+my %VIFInfo = (
+ VIF_ENERGY_WATT => { # 10(nnn-3) Wh 0.001Wh to 10000Wh
+ typeMask => 0b01111000,
+ expMask => 0b00000111,
+ type => 0b00000000,
+ bias => -3,
+ unit => 'Wh',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_ENERGY_JOULE => { # 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_MASS_FLOW => { # 10(nnn-3) kg/h 0.001kg/h to 10000kg/h
+ typeMask => 0b01111000,
+ expMask => 0b00000111,
+ type => 0b01010000,
+ bias => -3,
+ unit => 'kg/h',
+ 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_PRESSURE => { # 10(nn-3) bar 1mbar to 1000mbar
+ typeMask => 0b01111100,
+ expMask => 0b00000011,
+ type => 0b01101000,
+ bias => -3,
+ unit => 'bar',
+ 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 => { # Unit for Heat Cost Allocator, dimensonless
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01101110,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_FABRICATION_NO => { # Fabrication No
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01111000,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcAscii,
+ },
+ VIF_OWNER_NO => { # Eigentumsnummer (used by Easymeter even though the standard allows this only for writing to a slave)
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01111001,
+ bias => 0,
+ unit => '',
+ },
+ VIF_AVERAGING_DURATION_SEC => { # seconds
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110000,
+ bias => 0,
+ unit => 'sec',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_AVERAGING_DURATION_MIN => { # minutes
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110001,
+ bias => 0,
+ unit => 'min',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_AVERAGING_DURATION_HOURS => { # hours
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110010,
+ bias => 0,
+ unit => 'hours',
+ },
+ VIF_AVERAGING_DURATION_DAYS => { # days
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110011,
+ bias => 0,
+ unit => 'days',
+ },
+ VIF_ACTUALITY_DURATION_SEC => { # seconds
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110100,
+ bias => 0,
+ unit => 'sec',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_ACTUALITY_DURATION_MIN => { # minutes
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110101,
+ bias => 0,
+ unit => 'min',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_ACTUALITY_DURATION_HOURS => { # hours
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110110,
+ bias => 0,
+ unit => 'hours',
+ },
+ VIF_ACTUALITY_DURATION_DAYS => { # days
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110111,
+ bias => 0,
+ unit => 'days',
+ },
+);
+
+# Codes used with extension indicator $FD, see 8.4.4 on page 80
+my %VIFInfo_FD = (
+ VIF_CREDIT => { # Credit of 10nn-3 of the nominal local legal currency units
+ typeMask => 0b01111100,
+ expMask => 0b00000011,
+ type => 0b00000000,
+ bias => -3,
+ unit => '€',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_DEBIT => { # Debit of 10nn-3 of the nominal local legal currency units
+ typeMask => 0b01111100,
+ expMask => 0b00000011,
+ type => 0b00000100,
+ bias => -3,
+ unit => '€',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_ACCESS_NO => { # Access number (transmission count)
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001000,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_MEDIUM => { # Medium (as in fixed header)
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001001,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcAscii,
+ },
+ VIF_MANUFACTURER => { # Manufacturer (as in fixed header)
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001010,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_PARAMETER_SET_ID => { # Parameter set identification
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001011,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_MODEL_VERSION => { # Model / Version
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001100,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_HARDWARE_VERSION => { # Hardware version #
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001101,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_FIRMWARE_VERSION => { # Firmware version #
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001110,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_SOFTWARE_VERSION => { # Software version #
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001111,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+
+
+ VIF_CUSTOMER_LOCATION => { # Customer location
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010000,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+ VIF_CUSTOMER_CUSTOMER => { # Customer
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010001,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+ VIF_ACCESS_CODE_USER => { # Access code user
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010010,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+ VIF_ACCESS_CODE_OPERATOR => { # Access code operator
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010011,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+ VIF_ACCESS_CODE_SYSTEM_OPERATOR => { # Access code system operator
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010100,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+ VIF_PASSWORD => { # Password
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010110,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex
+ },
+
+ VIF_ERROR_FLAGS => { # Error flags (binary)
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010111,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcHex,
+ },
+ VIF_DURATION_SINCE_LAST_READOUT => { # Duration since last readout [sec(s)..day(s)]
+ typeMask => 0b01111100,
+ expMask => 0b00000011,
+ type => 0b00101100,
+ bias => 0,
+ unit => 's',
+ calcFunc => \&valueCalcTimeperiod,
+ },
+ VIF_VOLTAGE => { # 10nnnn-9 Volts
+ typeMask => 0b01110000,
+ expMask => 0b00001111,
+ type => 0b01000000,
+ bias => -9,
+ unit => 'V',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_ELECTRICAL_CURRENT => { # 10nnnn-12 Ampere
+ typeMask => 0b01110000,
+ expMask => 0b00001111,
+ type => 0b01010000,
+ bias => -12,
+ unit => 'A',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_RECEPTION_LEVEL => { # reception level of a received radio device.
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01110001,
+ bias => 0,
+ unit => 'dBm',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_STATE_PARAMETER_ACTIVATION => { # State of parameter activation
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01100110,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_SPECIAL_SUPPLIER_INFORMATION => { # Special supplier information
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01100111,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcNumeric,
+ },
+
+ VIF_FD_RESERVED => { # Reserved
+ typeMask => 0b01110000,
+ expMask => 0b00000000,
+ type => 0b01110000,
+ bias => 0,
+ unit => 'Reserved',
+ },
+
+);
+
+# 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,
+ },
+);
+
+
+# Codes used for an enhancement of VIFs other than $FD and $FB
+my %VIFInfo_other = (
+ VIF_ERROR_NONE => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00000000,
+ bias => 0,
+ unit => 'No error',
+ },
+ VIF_TOO_MANY_DIFES => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00000001,
+ bias => 0,
+ unit => 'Too many DIFEs',
+ },
+
+ VIF_ILLEGAL_VIF_GROUP => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00001100,
+ bias => 0,
+ unit => 'Illegal VIF-Group',
+ },
+
+ VIF_DATA_UNDERFLOW => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00010111,
+ bias => 0,
+ unit => 'Data underflow',
+ },
+
+
+ VIF_PER_SECOND => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100000,
+ bias => 0,
+ unit => 'per second',
+ },
+ VIF_PER_MINUTE => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100001,
+ bias => 0,
+ unit => 'per minute',
+ },
+ VIF_PER_HOUR => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100010,
+ bias => 0,
+ unit => 'per hour',
+ },
+ VIF_PER_DAY => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100011,
+ bias => 0,
+ unit => 'per day',
+ },
+ VIF_PER_WEEK => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100100,
+ bias => 0,
+ unit => 'per week',
+ },
+ VIF_PER_MONTH => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100101,
+ bias => 0,
+ unit => 'per month',
+ },
+ VIF_PER_YEAR => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100110,
+ bias => 0,
+ unit => 'per year',
+ },
+ VIF_PER_REVOLUTION => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00100111,
+ bias => 0,
+ unit => 'per revolution/measurement',
+ },
+ VIF_PER_INCREMENT_INPUT => {
+ typeMask => 0b01111110,
+ expMask => 0b00000000,
+ type => 0b00101000,
+ bias => 0,
+ unit => 'increment per input pulse on input channnel #',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_PER_INCREMENT_OUTPUT => {
+ typeMask => 0b01111110,
+ expMask => 0b00000000,
+ type => 0b00101010,
+ bias => 0,
+ unit => 'increment per output pulse on output channnel #',
+ calcFunc => \&valueCalcNumeric,
+ },
+ VIF_PER_LITER => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00101100,
+ bias => 0,
+ unit => 'per liter',
+ },
+ VIF_PER_M3 => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00101101,
+ bias => 0,
+ unit => 'per m³',
+ },
+ VIF_PER_KG => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00101110,
+ bias => 0,
+ unit => 'per kg',
+ },
+ VIF_PER_K => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00101111,
+ bias => 0,
+ unit => 'per K',
+ },
+ VIF_PER_KWH => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110000,
+ bias => 0,
+ unit => 'per kWh',
+ },
+ VIF_PER_GJ => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110001,
+ bias => 0,
+ unit => 'per GJ',
+ },
+ VIF_PER_KW => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110010,
+ bias => 0,
+ unit => 'per kW',
+ },
+ VIF_PER_KL => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110011,
+ bias => 0,
+ unit => 'per (K*l)',
+ },
+ VIF_PER_V => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110100,
+ bias => 0,
+ unit => 'per V',
+ },
+ VIF_PER_A => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110101,
+ bias => 0,
+ unit => 'per A',
+ },
+ VIF_PER_MULT_S => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110110,
+ bias => 0,
+ unit => 'multiplied by sek',
+ },
+ VIF_PER_MULT_SV => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00110111,
+ bias => 0,
+ unit => 'multiplied by sek / V',
+ },
+ VIF_PER_MULT_SA => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00111000,
+ bias => 0,
+ unit => 'multiplied by sek / A',
+ },
+
+ VIF_START_DATE_TIME => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00111001,
+ bias => 0,
+ unit => 'start date(/time) of',
+ },
+
+ VIF_ACCUMULATION_IF_POSITIVE => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b00111011,
+ bias => 0,
+ unit => 'Accumulation only if positive contribution',
+ },
+
+ VIF_DURATION_NO_EXCEEDS => {
+ typeMask => 0b01110111,
+ expMask => 0b00000000,
+ type => 0b01000001,
+ bias => 0,
+ unit => '# of exceeds',
+ calcFunc => \&valueCalcu,
+ },
+
+ VIF_DURATION_LIMIT_EXCEEDED => {
+ typeMask => 0b01110000,
+ expMask => 0b00000000,
+ type => 0b01010000,
+ bias => 0,
+ unit => 'duration of limit exceeded',
+ calcFunc => \&valueCalcufnn,
+ },
+
+ VIF_MULTIPLICATIVE_CORRECTION_FACTOR => {
+ typeMask => 0b01111000,
+ expMask => 0b00000111,
+ type => 0b01110000,
+ bias => -6,
+ unit => '',
+ },
+ VIF_MULTIPLICATIVE_CORRECTION_FACTOR_1000 => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01111101,
+ bias => 0,
+ unit => '',
+ calcFunc => \&valueCalcMultCorr1000,
+ },
+ VIF_FUTURE_VALUE => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01111110,
+ bias => 0,
+ unit => '',
+ },
+ VIF_MANUFACTURER_SPECIFIC => {
+ typeMask => 0b01111111,
+ expMask => 0b00000000,
+ type => 0b01111111,
+ bias => 0,
+ unit => 'manufacturer specific',
+ },
+
+);
+
+# For Easymeter (manufacturer specific)
+my %VIFInfo_ESY = (
+ VIF_ELECTRIC_POWER_PHASE => {
+ typeMask => 0b01000000,
+ expMask => 0b00000000,
+ type => 0b00000000,
+ bias => -2,
+ unit => 'W',
+ calcFunc => \&valueCalcNumeric,
+ },
+);
+
+my %VIFInfo_ESY2 = (
+ VIF_ELECTRIC_POWER_PHASE_NO => {
+ typeMask => 0b01111110,
+ expMask => 0b00000000,
+ type => 0b00101000,
+ bias => 0,
+ unit => 'phase #',
+ calcFunc => \&valueCalcNumeric,
+ },
+);
+
+# For Kamstrup (manufacturer specific)
+my %VIFInfo_KAM = (
+ VIF_KAMSTRUP_INFO => {
+ typeMask => 0b00000000,
+ expMask => 0b00000000,
+ type => 0b00000000,
+ bias => 0,
+ unit => '',
+ },
+);
+
+
+# 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',
+ 0x1d => 'Reserved for sensors',
+ 0x1e => 'Reserved for sensors',
+ 0x1f => 'Reserved for sensors',
+ 0x20 => 'Breaker (electricity)',
+ 0x21 => 'Valve (gas)',
+ 0x22 => 'Reserved for switching devices',
+ 0x23 => 'Reserved for switching devices',
+ 0x24 => 'Reserved for switching devices',
+ 0x25 => 'Customer unit (Display device)',
+ 0x26 => 'Reserved for customer units',
+ 0x27 => 'Reserved for customer units',
+ 0x28 => 'Waste water',
+ 0x29 => 'Garbage',
+ 0x2a => 'Carbon dioxide',
+ 0x2b => 'Environmental meter',
+ 0x2c => 'Environmental meter',
+ 0x2d => 'Environmental meter',
+ 0x2e => 'Environmental meter',
+ 0x2f => 'Environmental meter',
+ 0x31 => 'OMS MUC',
+ 0x32 => 'OMS unidirectional repeater',
+ 0x33 => 'OMS bidirectional repeater',
+ 0x37 => 'Radio converter (Meter side)',
+);
+
+
+# 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',
+);
+
+my %functionFieldTypes = (
+ 0b00 => 'Instantaneous value',
+ 0b01 => 'Maximum value',
+ 0b10 => 'Minimum value',
+ 0b11 => 'Value during error state',
+);
+
+
+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 calcCRC($$) {
+ 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 $blocksize = LL_BLOCK_SIZE;
+ my $blocksize_with_crc = LL_BLOCK_SIZE + $self->{crc_size};
+ my $crcoffset;
+
+ my $msgLen = $self->{datalen}; # size without CRCs
+ my $noOfBlocks = $self->{datablocks}; # total number of data blocks, each with a CRC appended
+ my $rest = $msgLen % LL_BLOCK_SIZE; # size of the last data block, can be smaller than 16 bytes
+
+
+ #print "crc_size $self->{crc_size}\n";
+
+ return $msg if $self->{crc_size} == 0;
+
+ # each block is 16 bytes + 2 bytes CRC
+
+ #print "Länge $msgLen Anz. Blöcke $noOfBlocks rest $rest\n";
+
+ for ($i=0; $i < $noOfBlocks; $i++) {
+ $crcoffset = $blocksize_with_crc * $i + LL_BLOCK_SIZE;
+ #print "$i: crc offset $crcoffset\n";
+ if ($rest > 0 && $crcoffset + $self->{crc_size} > ($noOfBlocks - 1) * $blocksize_with_crc + $rest) {
+ # last block is smaller
+ $crcoffset = ($noOfBlocks - 1) * $blocksize_with_crc + $rest;
+ #print "last crc offset $crcoffset\n";
+ $blocksize = $msgLen - ($i * $blocksize);
+ }
+
+ $crc = unpack('n',substr($msg, $crcoffset, $self->{crc_size}));
+ #printf("%d: CRC %x, calc %x blocksize $blocksize\n", $i, $crc, $self->calcCRC(substr($msg, $blocksize_with_crc*$i, $blocksize)));
+ if ($crc != $self->calcCRC(substr($msg, $blocksize_with_crc*$i, $blocksize))) {
+ $self->{errormsg} = "crc check failed for block $i";
+ $self->{errorcode} = ERR_CRC_FAILED;
+ return 0;
+ }
+ $res .= substr($msg, $blocksize_with_crc*$i, $blocksize);
+ }
+
+ return $res;
+}
+
+
+sub manId2hex($$)
+{
+ my $class = 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 $class = 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->{crc_size} = CRC_SIZE;
+ $self->{frame_type} = FRAME_TYPE_A; # default
+}
+
+sub setCRCsize {
+ my $self = shift;
+
+ $self->{crc_size} = shift;
+}
+
+sub getCRCsize {
+ my $self = shift;
+
+ return $self->{crc_size};
+}
+
+sub decodeConfigword($) {
+ my $self = shift;
+
+
+ #printf("cw: %01x %01x\n", $self->{cw_1}, $self->{cw_2});
+ $self->{cw_parts}{mode} = ($self->{cw_2} & 0b00011111);
+ #printf("mode: %02x\n", $self->{cw_parts}{mode});
+ if ($self->{cw_parts}{mode} == 5 || $self->{cw_parts}{mode} == 0) {
+ $self->{cw_parts}{bidirectional} = ($self->{cw_2} & 0b10000000) >> 7;
+ $self->{cw_parts}{accessability} = ($self->{cw_2} & 0b01000000) >> 6;
+ $self->{cw_parts}{synchronous} = ($self->{cw_2} & 0b00100000) >> 5;
+ $self->{cw_parts}{encrypted_blocks} = ($self->{cw_1} & 0b11110000) >> 4;
+ $self->{cw_parts}{content} = ($self->{cw_1} & 0b00001100) >> 2;
+ $self->{cw_parts}{repeated_access} = ($self->{cw_1} & 0b00000010) >> 1;
+ $self->{cw_parts}{hops} = ($self->{cw_1} & 0b00000001);
+ } elsif ($self->{cw_parts}{mode} == 7) {
+ # configword ist 3 Bytes lang
+ $self->{cw_parts}{key_id} = ($self->{cw_3} & 0b00001111);
+ $self->{cw_parts}{dynamic_key} = ($self->{cw_3} & 0b01110000) >> 4;
+ $self->{cw_parts}{content} = ($self->{cw_2} & 0b11000000) >> 6;
+ $self->{cw_parts}{encrypted_blocks} = ($self->{cw_1} & 0b11110000) >> 4;
+ }
+}
+
+sub decodeBCD($$$) {
+ my $self = shift;
+ my $digits = shift;
+ my $bcd = shift;
+ my $byte;
+ my $val=0;
+ my $mult=1;
+
+ #print "bcd:" . unpack("H*", $bcd) . "\n";
+
+ 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 findVIF($$$) {
+ my $vif = shift;
+ my $vifInfoRef = shift;
+ my $dataBlockRef = shift;
+ my $bias;
+
+ if (defined $vifInfoRef) {
+ VIFID: foreach my $vifType ( keys %$vifInfoRef ) {
+
+ #printf "vifType $vifType VIF $vif typeMask $vifInfoRef->{$vifType}{typeMask} type $vifInfoRef->{$vifType}{type}\n";
+
+ if (($vif & $vifInfoRef->{$vifType}{typeMask}) == $vifInfoRef->{$vifType}{type}) {
+ #printf " match vif %02x vifType %s\n", $vif, $vifType;
+ $dataBlockRef->{vif} = $vif;
+
+ $bias = $vifInfoRef->{$vifType}{bias};
+ $dataBlockRef->{exponent} = $vif & $vifInfoRef->{$vifType}{expMask};
+
+ $dataBlockRef->{type} = $vifType;
+ $dataBlockRef->{unit} = $vifInfoRef->{$vifType}{unit};
+ if (defined $dataBlockRef->{exponent} && defined $bias) {
+ $dataBlockRef->{valueFactor} = 10 ** ($dataBlockRef->{exponent} + $bias);
+ } else {
+ $dataBlockRef->{valueFactor} = 1;
+ }
+ $dataBlockRef->{calcFunc} = $vifInfoRef->{$vifType}{calcFunc};
+
+ #printf("type %s bias %d exp %d valueFactor %d unit %s\n", $dataBlockRef->{type}, $bias, $dataBlockRef->{exponent}, $dataBlockRef->{valueFactor},$dataBlockRef->{unit});
+ return 1;
+ }
+ }
+ #printf "no match!\n";
+ return 0;
+ }
+ return 1;
+}
+
+sub decodePlaintext($$$) {
+ my $self = shift;
+ my $vib = shift;
+ my $dataBlockRef = shift;
+ my $offset = shift;
+ my $vifLength = unpack('C', substr($vib,$offset++,1));
+
+ $dataBlockRef->{type} = "see unit";
+ $dataBlockRef->{unit} = substr($vib, $offset, $vifLength);
+ $dataBlockRef->{unit} = reverse($dataBlockRef->{unit}) unless $self->{mode_bit};
+ $offset += $vifLength;
+ return $offset;
+}
+
+sub decodeValueInformationBlock($$$) {
+ my $self = shift;
+ my $vib = shift;
+ my $dataBlockRef = shift;
+
+ my $offset = 0;
+ my $vif;
+ my $vifInfoRef;
+ my $vifExtension = 0;
+ my $vifExtNo = 0;
+ my $isExtension;
+ my $dataBlockExt;
+ my @VIFExtensions = ();
+ my $analyzeVIF = 1;
+
+ $dataBlockRef->{type} = '';
+ # The unit and multiplier is taken from the table for primary VIF
+ $vifInfoRef = \%VIFInfo;
+
+
+ EXTENSION: while (1) {
+ $vif = unpack('C', substr($vib,$offset++,1));
+ $isExtension = $vif & VIF_EXTENSION_BIT;
+ #printf("vif: %x isExtension %d\n", $vif, $isExtension);
+ if ($isExtension) {
+ $dataBlockRef->{vif} = $vif;
+ }
+
+ # Is this an extension?
+ last EXTENSION if (!$isExtension);
+
+ # yes, process extension
+
+ $vifExtNo++;
+ if ($vifExtNo > 10) {
+ $dataBlockRef->{errormsg} = 'too many VIFE';
+ $dataBlockRef->{errorcode} = ERR_TOO_MANY_VIFE;
+ last EXTENSION;
+ }
+
+ # switch to extension codes
+ $vifExtension = $vif;
+ $vif &= ~VIF_EXTENSION_BIT;
+ #printf("vif ohne extension: %x\n", $vif);
+ if ($vif == 0x7D) {
+ $vifInfoRef = \%VIFInfo_FD;
+ } elsif ($vif == 0x7B) {
+ $vifInfoRef = \%VIFInfo_FB;
+ } elsif ($vif == 0x7C) {
+ # Plaintext VIF
+ $offset = $self->decodePlaintext($vib, $dataBlockRef, $offset);
+ $analyzeVIF = 0;
+ last EXTENSION;
+ } elsif ($vif == 0x7F) {
+
+ if ($self->{manufacturer} eq 'ESY') {
+ # Easymeter
+ $vif = unpack('C', substr($vib,$offset++,1));
+ #printf("ESY VIF %x\n", $vif);
+ $vifInfoRef = \%VIFInfo_ESY;
+ } elsif ($self->{manufacturer} eq 'KAM') {
+ # Kamstrup
+ $vif = unpack('C', substr($vib,$offset++,1));
+ $vifInfoRef = \%VIFInfo_KAM;
+ } else {
+ # manufacturer specific data, can't be interpreted
+
+ $dataBlockRef->{type} = VIF_TYPE_MANUFACTURER_SPECIFIC;
+ $dataBlockRef->{unit} = "";
+ $analyzeVIF = 0;
+ }
+ last EXTENSION;
+ } else {
+ # enhancement of VIFs other than $FD and $FB (see page 84ff.)
+ #print "other extension\n";
+ $dataBlockExt = {};
+ if ($self->{manufacturer} eq 'ESY') {
+ #print "ESY\n";
+ $vifInfoRef = \%VIFInfo_ESY2;
+ $dataBlockExt->{value} = unpack('C',substr($vib,2,1)) * 100;
+ } else {
+ $dataBlockExt->{value} = $vif;
+ $vifInfoRef = \%VIFInfo_other;
+ }
+
+ if (findVIF($vif, $vifInfoRef, $dataBlockExt)) {
+ push @VIFExtensions, $dataBlockExt;
+ } else {
+ $dataBlockRef->{type} = 'unknown';
+ $dataBlockRef->{errormsg} = "unknown VIFE " . sprintf("%x", $vifExtension) . " at offset " . ($offset-1);
+ $dataBlockRef->{errorcode} = ERR_UNKNOWN_VIFE;
+ }
+ }
+ last EXTENSION if (!$isExtension);
+ }
+
+ if ($analyzeVIF) {
+ if ($vif == 0x7C) {
+ # Plaintext VIF
+ $offset = $self->decodePlaintext($vib, $dataBlockRef, $offset);
+ } elsif (findVIF($vif, $vifInfoRef, $dataBlockRef) == 0) {
+ $dataBlockRef->{errormsg} = "unknown VIFE " . sprintf("%x", $vifExtension) . " at offset " . ($offset-1);
+ $dataBlockRef->{errorcode} = ERR_UNKNOWN_VIFE;
+ }
+ }
+ $dataBlockRef->{VIFExtensions} = \@VIFExtensions;
+
+ if ($dataBlockRef->{type} eq '') {
+ $dataBlockRef->{type} = 'unknown';
+ $dataBlockRef->{errormsg} = sprintf("in VIFExtension %x unknown VIF %x",$vifExtension, $vif);
+ $dataBlockRef->{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;
+
+ $dataBlockRef->{dif} = $dif;
+
+ #printf("dif %02x 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) {
+ $dataBlockRef->{errormsg} = 'too many DIFE';
+ $dataBlockRef->{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);
+ #printf("dife %x extno %d storage %d\n", $dif, $difExtNo, $storageNo);
+ }
+
+ $dataBlockRef->{functionField} = $functionField;
+ $dataBlockRef->{functionFieldText} = $functionFieldTypes{$functionField};
+ $dataBlockRef->{dataField} = $df;
+ $dataBlockRef->{storageNo} = $storageNo;
+ $dataBlockRef->{tariff} = $tariff;
+ $dataBlockRef->{devUnit} = $devUnit;
+
+ #printf("in DIF: datafield %x\n", $dataBlockRef->{dataField});
+ #print "offset in dif $offset\n";
+ 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;
+ $dataBlock->{unit} = '';
+
+ 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);
+
+ # DIF_INT are _signed_ values
+
+ if ($dataBlock->{dataField} == DIF_NONE or $dataBlock->{dataField} == DIF_READOUT) {
+ $dataBlockNo--;
+ $offset++;
+ } 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_BCD12) {
+ $value = $self->decodeBCD(12, substr($payload,$offset,6));
+ $offset += 6;
+ } elsif ($dataBlock->{dataField} == DIF_INT8) {
+ $value = unpack('c', substr($payload, $offset, 1));
+ $offset += 1;
+ } elsif ($dataBlock->{dataField} == DIF_INT16) {
+ $value = unpack('s<', substr($payload, $offset, 2));
+ $offset += 2;
+ } elsif ($dataBlock->{dataField} == DIF_INT24) {
+ my @bytes = unpack('CCC', substr($payload, $offset, 3));
+ $offset += 3;
+ $value = $bytes[0] + $bytes[1] << 8 + $bytes[2] << 16;
+ # two's complement
+ $value = ~$value + 1;
+ } elsif ($dataBlock->{dataField} == DIF_INT32) {
+ $value = unpack('l<', substr($payload, $offset, 4));
+ $offset += 4;
+ } elsif ($dataBlock->{dataField} == DIF_INT48) {
+ my @words = unpack('vvv', substr($payload, $offset, 6));
+ $value = $words[0] + ($words[1] << 16) + ($words[2] << 32);
+ # two's complement
+ $value = ~$value + 1;
+ $offset += 6;
+ } elsif ($dataBlock->{dataField} == DIF_INT64) {
+ $value = unpack('q<', substr($payload, $offset, 8));
+ $offset += 8;
+ } elsif ($dataBlock->{dataField} == DIF_FLOAT32) {
+ #not allowed according to wmbus standard, Qundis seems to use it nevertheless
+ $value = unpack('f', substr($payload, $offset, 4));
+ $offset += 4;
+ } elsif ($dataBlock->{dataField} == DIF_VARLEN) {
+ my $lvar = unpack('C',substr($payload, $offset++, 1));
+ #print "in datablock $dataBlockNo: LVAR field " . sprintf("%x", $lvar) . "\n";
+ #printf "payload len %d offset %d\n", length($payload), $offset;
+ if ($lvar <= 0xbf) {
+ if ($dataBlock->{type} eq "MANUFACTURER SPECIFIC") {
+ # special handling, LSE seems to lie about this
+ $value = unpack('H*',substr($payload, $offset, $lvar));
+ #print "VALUE: " . $value . "\n";
+ } else {
+ # ASCII string with LVAR characters
+ $value = valueCalcAscii(substr($payload, $offset, $lvar), $dataBlock);
+
+ if ($self->{manufacturer} eq 'ESY') {
+ # Easymeter stores the string backwards!
+ $value = reverse($value);
+ }
+ }
+ $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
+ #print "DIF_SPECIAL at $offset\n";
+ $value = unpack("H*", substr($payload,$offset));
+ 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";
+ } elsif (defined $value) {
+ $dataBlock->{value} = $value;
+ } else {
+ $dataBlock->{value} = "";
+ }
+
+ my $VIFExtensions = $dataBlock->{VIFExtensions};
+ for my $VIFExtension (@$VIFExtensions) {
+ $dataBlock->{extension_unit} = $VIFExtension->{unit};
+ #printf("extension unit %s\n", $dataBlock->{extension_unit});
+ if (defined $VIFExtension->{calcFunc}) {
+ #printf("Extension value %d, valueFactor %d\n", $VIFExtension->{value}, $VIFExtension->{valueFactor});
+ $dataBlock->{extension_value} = $VIFExtension->{calcFunc}->($VIFExtension->{value}, $dataBlock);
+ } elsif (defined $VIFExtension->{value}) {
+ $dataBlock->{extension_value} = sprintf("%x",$VIFExtension->{value});
+ } else {
+ #$dataBlock->{extension} = "";
+ }
+ }
+ undef $value;
+
+ push @dataBlocks, $dataBlock;
+ }
+
+ $self->{datablocks} = \@dataBlocks;
+ return 1;
+}
+
+sub decrypt_mode5($) {
+ my $self = shift;
+ my $encrypted = shift;
+ my $padding = 2;
+
+ # see 4.2.5.3, page 26
+ my $initVector = substr($self->{msg},2,8);
+ for (1..8) {
+ $initVector .= pack('C',$self->{access_no});
+ }
+
+ if (length($encrypted)%16 == 0) {
+ # no padding if data length is multiple of blocksize
+ $padding = 0;
+ } else {
+ $padding = 2;
+ }
+ #printf("length encrypted %d padding %d\n", length($encrypted), $padding);
+ my $cipher = Crypt::Mode::CBC->new('AES', $padding);
+ return $cipher->decrypt($encrypted, $self->{aeskey}, $initVector);
+}
+
+sub decrypt_mode7($) {
+ my $self = shift;
+ my $encrypted = shift;
+ my $padding = 2;
+ my $identno;
+
+ # generate dynamic key
+ my $cmac = Digest::CMAC->new($self->{aeskey});
+ #my $cmac = Digest::CMAC->new(pack("H*",'000102030405060708090A0B0C0D0E0F'));
+
+ # The calculation of Kenc and Kmac for the meter:
+ # Kenc = CMAC(MK, 0x00 ||C[7..0] ||C[15..8] ||C[23..16] ||C[31..24] ||ID_0||ID_1||ID_2||ID_3||0x07||0x07||0x07||0x07||0x07||0x07||0x07)
+ # Where C[7..0] is the LSB and C[31..24] is the MSB (Big Endian) of the counter AFL.MCR.C from meter to other (gateway).
+
+ $cmac->add(pack("H*", "00"));
+
+ #$self->{afl}{mcr} = pack("H*", "b30a0000");
+ $cmac->add($self->{afl}{mcr});
+ #print "MCR " . unpack("H*", $self->{afl}{mcr}) . "\n";
+ if (exists($self->{meter_id_raw})) {
+ $identno = $self->{meter_id_raw};
+ } else {
+ $identno = $self->{afield_identno};
+ }
+ #print "identno " . unpack("H*", $identno) . "\n";
+ $cmac->add($identno);
+ $cmac->add(pack("H*", "07070707070707"));
+ #$cmac->add(pack("H*",'7856341207070707070707'));
+
+
+
+ my $key = $cmac->digest;
+
+ #printf("Dynamic key %s\n", $cmac->hexdigest);
+
+ # see 9.2.4, page 59
+ my $initVector = '';
+ for (1..16) {
+ $initVector .= pack('C',0x00);
+ }
+ if (length($encrypted)%16 == 0) {
+ # no padding if data length is multiple of blocksize
+ $padding = 0;
+ } else {
+ $padding = 2;
+ }
+
+ #$encrypted = pack("H*","9058475F4BC91DF878B80A1B0F98B629024AAC727942BFC549233C0140829B93");
+ #print unpack("H*", $encrypted) . "\n";
+ my $cipher = Crypt::Mode::CBC->new('AES', $padding);
+ return $cipher->decrypt($encrypted, $key, $initVector);
+}
+
+sub decodeAFL($$) {
+ my $self = shift;
+ my $afl = shift;
+ my $offset = 0;
+
+
+ $self->{afl}{fcl} = unpack('v', $afl);
+ $offset += 2;
+ $self->{afl}{fcl_mf} = ($self->{afl}{fcl} & 0b0100000000000000) != 0;
+ $self->{afl}{fcl_mclp} = ($self->{afl}{fcl} & 0b0010000000000000) != 0;
+ $self->{afl}{fcl_mlp} = ($self->{afl}{fcl} & 0b0001000000000000) != 0;
+ $self->{afl}{fcl_mcrp} = ($self->{afl}{fcl} & 0b0000100000000000) != 0;
+ $self->{afl}{fcl_macp} = ($self->{afl}{fcl} & 0b0000010000000000) != 0;
+ $self->{afl}{fcl_kip} = ($self->{afl}{fcl} & 0b0000001000000000) != 0;
+ $self->{afl}{fcl_fid} = $self->{afl}{fcl} & 0b0000000011111111;
+
+ if ($self->{afl}{fcl_mclp}) {
+ # AFL Message Control Field (AFL.MCL)
+ $self->{afl}{mcl} = unpack('C', substr($afl, $offset, 1));
+ #printf "AFL MCL %01x\n", $self->{afl}{mcl};
+ $offset += 1;
+ $self->{afl}{mcl_mlmp} = ($self->{afl}{mcl} & 0b01000000) != 0;
+ $self->{afl}{mcl_mcmp} = ($self->{afl}{mcl} & 0b00100000) != 0;
+ $self->{afl}{mcl_kimp} = ($self->{afl}{mcl} & 0b00010000) != 0;
+ $self->{afl}{mcl_at} = ($self->{afl}{mcl} & 0b00001100) >> 2;
+ $self->{afl}{mcl_ato} = ($self->{afl}{mcl} & 0b00000011);
+ }
+ if ($self->{afl}{fcl_mcrp}) {
+ # AFL Message Counter Field (AFL.MCR)
+ #$self->{afl}{mcr} = unpack('N', substr($afl, $offset));
+ $self->{afl}{mcr} = substr($afl, $offset, 4);
+ $offset += 4;
+ }
+ if ($self->{afl}{fcl_mlp}) {
+ # AFL Message Length Field (AFL.ML)
+ $self->{afl}{ml} = unpack('v', substr($afl, $offset));
+ $offset += 2;
+ }
+ if ($self->{afl}{mcl_at} == 1) {
+ # CMAC-AES128 (see 9.3.1)
+ my $mac_len = 0;
+ if ($self->{afl}{mcl_ato} == 1) {
+ $mac_len = 8;
+ $self->{afl}{mac} = (unpack('N', substr($afl, $offset, 4))) << 32 | ((unpack('N', substr($afl, $offset+4, 4))));
+ #printf "AFL MAC %8x\n", $self->{afl}{mac};
+ } else {
+ # reserved
+ }
+ $offset += $mac_len;
+ }
+ if ($self->{afl}{fcl_kip}) {
+ # AFL Key Information-Field (AFL.KI)
+ $self->{afl}{ki} = unpack('v', $afl);
+ $self->{afl}{ki_key_version} = ($self->{afl}{ki} & 0b1111111100000000) >> 8;
+ $self->{afl}{ki_kdf_selection} = ($self->{afl}{ki} & 0b0000000001110000) >> 4;
+ $self->{afl}{ki_key_id} = ($self->{afl}{ki} & 0b0000000000001111);
+ $offset += 2;
+ }
+ return $offset;
+}
+
+sub decodeCompactFrame($$)
+{
+ my $self = shift;
+ my $compact = shift;
+ my $applicationlayer = "";
+
+ # VIF depends on the resolution of the volume register
+ # 13 = 3 decimals
+ # 14 = 2 decimals
+ # 15 = 1 decimal
+ # 16 = 0 decimals
+ # functionField part of DIF is also variable, at least for temperatures
+ # all in all that would be 4 * 4 (for vif) * 4 * 4 (for dif) * 3 (type of telegram) combinations (768)
+ # for now only search for those that are documented or habe been observed in real telegrams
+ for my $vif ("13","14","15","16") {
+ #printf("compact frame $vif\n");
+ if ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif"))) {
+ # Info, Volume, Target Volume
+ # convert into full frame
+ $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
+ . pack("H*", "04$vif") . substr($compact,7,4) # volume
+ . pack("H*", "44$vif") . substr($compact,11,4); # target volume
+ last;
+ } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "523B"))) {
+ # Info, Volume, Max flow
+ # convert into full frame
+ $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
+ . pack("H*", "04$vif") . substr($compact,7,4) # volume
+ . pack("H*", "523B") . substr($compact,11,2); # max flow
+ last;
+ } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif" . "615B" . "6167"))) {
+ # Info, Volume, Max flow, min flow temp, max external temp
+ # convert into full frame
+ $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
+ . pack("H*", "04$vif") . substr($compact,7,4) # volume
+ . pack("H*", "44$vif") . substr($compact,11,4) # target volume
+ . pack("H*", "615B") . substr($compact,15,1) # flow temp
+ . pack("H*", "6167") . substr($compact,16,1); # external temp
+ last;
+ } elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "44$vif" . "615B" . "5167"))) {
+ # Info, Volume, Max flow, min flow temp, max external temp
+ # convert into full frame
+ $applicationlayer = pack("H*", "02FF20") . substr($compact, 5, 2) # Info
+ . pack("H*", "04$vif") . substr($compact,7,4) # volume
+ . pack("H*", "44$vif") . substr($compact,11,4) # target volume
+ . pack("H*", "615B") . substr($compact,15,1) # flow temp
+ . pack("H*", "5167") . substr($compact,16,1); # external temp
+ last;
+ }
+ }
+ return $applicationlayer;
+}
+
+sub decodeApplicationLayer($) {
+ my $self = shift;
+ my $applicationlayer = $self->{applicationlayer};
+ my $payload;
+
+ #print unpack("H*", $applicationlayer) . "\n";
+
+ $self->{isEncrypted} = 0;
+ if ($self->{errorcode} != ERR_NO_ERROR) {
+ # CRC check failed
+ return 0;
+ }
+ $self->{cifield} = unpack('C', $applicationlayer);
+
+ my $offset = 1;
+ my $has_ell = 1;
+
+ if ($self->{cifield} == CI_ELL_2) {
+ # Extended Link Layer
+ ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset));
+ $offset += 2;
+ } elsif ($self->{cifield} == CI_ELL_10) {
+ # Extended Link Layer (long)
+ ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset));
+ $offset += 2;
+ $self->{ell}{manufacturer} = substr($applicationlayer,$offset, 2);
+ $offset += 2;
+ $self->{ell}{identno} = substr($applicationlayer,$offset, 4);
+ $offset += 4;
+ ($self->{ell}{version},$self->{ell}{device}) = unpack('CC', substr($applicationlayer,$offset));
+ $offset += 2;
+ } elsif ($self->{cifield} == CI_ELL_8) {
+ # Extended Link Layer, payload CRC is part of (encrypted) payload
+ ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{session_number}) = unpack('CCV', substr($applicationlayer, $offset));
+ $offset += 6;
+ } elsif ($self->{cifield} == CI_ELL_16) {
+ # Extended Link Layer
+ ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{m2}, $self->{ell}{a2}, $self->{ell}{session_number}) = unpack('CCvC6V', substr($applicationlayer,$offset));
+ $offset += 14;
+ } else {
+ $has_ell = 0;
+ }
+
+ if (exists($self->{ell}{session_number})) {
+ $self->{ell}{session_number_enc} = $self->{ell}{session_number} >> 29;
+ $self->{ell}{session_number_time} = ($self->{ell}{session_number} & 0b0001111111111111111111111111111) >> 4;
+ $self->{ell}{session_number_session} = $self->{ell}{session_number} & 0b1111;
+ $self->{isEncrypted} = $self->{ell}{session_number_enc} != 0;
+ $self->{decrypted} = 0;
+
+ if ($self->{isEncrypted}) {
+ if ($self->{aeskey}) {
+ if ($hasCTR) {
+ # AES IV
+ # M-field, A-field, CC, SN, 00, 0000
+ my $initVector = pack("v", $self->{mfield}) . $self->{afield} . pack("CV", $self->{ell}{cc}, $self->{ell}{session_number}) . pack("H*", "000000");
+ my $m = Crypt::Mode::CTR->new('AES', 1);
+ my $ciphertext = substr($applicationlayer,$offset); # payload CRC must also be decrypted
+ #printf("##ciphertext: %s\n", unpack("H*", $ciphertext));
+ $payload = $m->decrypt($ciphertext, $self->{aeskey}, $initVector);
+
+ #printf("##plaintext %s\n", unpack("H*", $payload));
+ } else {
+ $self->{errormsg} = 'Crypt::Mode::CTR is not installed, please install it (sudo cpan -i Crypt::Mode::CTR)';
+ $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
+ return 0;
+ }
+ } else {
+ $self->{errormsg} = 'encrypted message and no aeskey provided';
+ $self->{errorcode} = ERR_NO_AESKEY;
+ return 0;
+ }
+ }
+ $self->{ell}{crc} = unpack('v', $payload);
+ $offset += 2;
+ # PayloadCRC is a cyclic redundancy check covering the remainder of the frame (excluding the CRC fields)
+ # payload CRC is also encrypted
+ if ($self->{ell}{crc} != $self->calcCRC(substr($payload, 2, $self->{lfield}-20))) {
+ #printf("crc %x, calculated %x\n", $self->{ell}{crc}, $self->calcCRC(substr($payload, 2, $self->{lfield}-20)));
+ $self->{errormsg} = "Payload CRC check failed on ELL" . ($self->{isEncrypted} ? ", wrong AES key?" : "");
+ $self->{errorcode} = ERR_CRC_FAILED;
+ return 0;
+ } else {
+ $self->{decrypted} = 1;
+ }
+ $applicationlayer = $payload;
+ $offset = 2; # skip PayloadCRC
+ }
+
+ if ($offset > 1) {
+ $applicationlayer = substr($applicationlayer,$offset);
+ $self->{cifield} = unpack('C', $applicationlayer);
+ $offset = 1;
+ if ($self->{cifield} == CI_AFL) {
+ # Authentification and Fragmentation Layer
+ $self->{afl}{afll} = unpack('C', substr($applicationlayer, $offset));
+ #printf "AFL AFLL %02x\n", $self->{afl}{afll};
+ $offset += 1;
+ $self->decodeAFL(substr($applicationlayer,$offset,$self->{afl}{afll}));
+ $offset += $self->{afl}{afll};
+ if ($self->{afl}{fcl_mf}) {
+ $self->{errormsg} = "fragmented messages are not yet supported";
+ $self->{errorcode} = ERR_FRAGMENT_UNSUPPORTED;
+ return 0;
+ }
+ }
+ }
+
+ if ($offset > 1) {
+ $applicationlayer = substr($applicationlayer,$offset);
+ $self->{cifield} = unpack('C', $applicationlayer);
+ $offset = 1;
+ }
+
+ # initialize some fields
+ $self->{cw_1} = 0;
+ $self->{cw_2} = 0;
+ $self->{cw_3} = 0;
+ $self->{status} = 0;
+ $self->{statusstring} = "";
+ $self->{access_no} = 0;
+ $self->{sent_from_master} = 0;
+
+ #printf("CI Field %02x\n", $self->{cifield});
+
+ # Config Word ist normalerweise 2 Bytes lang, nur bei encryption mode 7 drei Bytes
+ # erstmal drei Bytes auslesen, aber den Offset nur um 2 Bytes erhöhen
+
+ if ($self->{cifield} == CI_RESP_4 || $self->{cifield} == CI_RESP_SML_4) {
+ # Short header
+ ($self->{access_no}, $self->{status}, $self->{cw_1}, $self->{cw_2}, $self->{cw_3}) = unpack('CCCCC', substr($applicationlayer,$offset));
+ #printf("Short header access_no %x\n", $self->{access_no});
+ $offset += 4;
+ } elsif ($self->{cifield} == CI_RESP_12 || $self->{cifield} == CI_RESP_SML_12) {
+ # Long header
+ $self->{meter_id_raw} = substr($applicationlayer,$offset,4);
+ ($self->{meter_man}, $self->{meter_vers}, $self->{meter_dev}, $self->{access_no}, $self->{status}, $self->{cw_1}, $self->{cw_2}, $self->{cw_3})
+ = unpack('vCCCCCCC', substr($applicationlayer,$offset+4));
+ $self->{meter_id} = sprintf("%08d", unpack('V', $self->{meter_id_raw}));
+ $self->{meter_devtypestring} = $validDeviceTypes{$self->{meter_dev}} || 'unknown';
+ $self->{meter_manufacturer} = uc($self->manId2ascii($self->{meter_man}));
+ #printf("Long header access_no %x\n", $self->{access_no});
+ $offset += 12;
+ } elsif ($self->{cifield} == CI_RESP_0 || $self->{cifield} == 0x30) {
+ # no header
+ #print "No header\n";
+
+ } elsif ($self->{cifield} == 0x79 && $self->{manufacturer} eq 'KAM') {
+ #print "Kamstrup compact frame header\n";
+ $self->{format_signature} = unpack("v", substr($applicationlayer,$offset, 2));
+ $offset += 2;
+ $self->{full_frame_payload_crc} = unpack("v", substr($applicationlayer, $offset, 2));
+ $offset += 2;
+ $applicationlayer = $self->decodeCompactFrame($applicationlayer);
+ if ($applicationlayer eq "") {
+ $self->{errormsg} = 'Unknown Kamstrup compact frame format';
+ $self->{errorcode} = ERR_UNKNOWN_COMPACT_FORMAT;
+ return 0;
+ } else {
+ $offset = 0;
+ }
+ if ($self->{full_frame_payload_crc} != $self->calcCRC($applicationlayer)) {
+ $self->{errormsg} = 'Kamstrup compact frame format payload CRC error';
+ $self->{errorcode} = ERR_CRC_FAILED;
+ return 0;
+ }
+ } elsif ($self->{cifield} == CI_SND_UD_MODE_1 || $self->{cifield} == CI_SND_UD_MODE_2) {
+ $self->{sent_from_master} = 1;
+ # The EN1434-3 defines two possible data sequences in multibyte records.
+ # The bit two (counting begins with bit 0, value 4), which is called M bit or Mode bit,
+ # in the CI field gives an information about the used byte sequence in multibyte data structures.
+ # If the Mode bit is not set (Mode 1), the least significant byte of a multibyte record is transmitted first,
+ # otherwise (Mode 2) the most significant byte.
+ # The Usergroup recommends to use only the Mode 1 in future applications.
+ $self->{mode_bit} = $self->{cifield} & 4;
+ } else {
+ # unsupported
+ $self->decodeConfigword();
+ $self->{errormsg} = 'Unsupported CI Field ' . sprintf("%x", $self->{cifield}) . ", remaining payload is " . unpack("H*", substr($applicationlayer,$offset));
+ $self->{errorcode} = ERR_UNKNOWN_CIFIELD;
+ return 0;
+ }
+ $self->{statusstring} = join(", ", $self->state2string($self->{status}));
+
+ $self->decodeConfigword();
+
+ $self->{encryptionMode} = $encryptionModes{$self->{cw_parts}{mode}};
+ if ($self->{cw_parts}{mode} == 0) {
+ # no encryption
+ if (!$self->{isEncrypted}) {
+ $self->{decrypted} = 1;
+ }
+ $payload = substr($applicationlayer, $offset);
+ } elsif ($self->{cw_parts}{mode} == 5 || $self->{cw_parts}{mode} == 7) {
+ # data is encrypted with AES 128, dynamic init vector
+ # decrypt data before further processing
+ $self->{isEncrypted} = 1;
+ $self->{decrypted} = 0;
+
+ if ($self->{aeskey}) {
+ if ($hasCBC) {
+ # payload can be only partially encrypted.
+ # decrypt only the encrypted part
+ my $encrypted_length = $self->{cw_parts}{encrypted_blocks} * 16;
+ if ($self->{cw_parts}{mode} == 5) {
+ #printf("encrypted payload %s\n", unpack("H*", substr($applicationlayer,$offset, $encrypted_length)));
+ eval {
+ $payload = $self->decrypt_mode5(substr($applicationlayer, $offset, $encrypted_length));
+ };
+ } else {
+ # mode 7
+ if ($hasCMAC) {
+ $offset++; # account for codeword byte 3
+ #printf("encrypted payload %s\n", unpack("H*", substr($applicationlayer,$offset, $encrypted_length)));
+ eval {
+ $payload = $self->decrypt_mode7(substr($applicationlayer, $offset, $encrypted_length));
+ }
+ } else {
+ $self->{errormsg} = 'Digest::CMAC is not installed, please install it (sudo cpan -i Digest::CMAC)';
+ $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
+ return 0;
+ }
+ }
+ if ($@) {
+ #fatal decryption error occurred
+ $self->{errormsg} = "fatal decryption error for mode " . $self->{cw_parts}{mode} . ": $@";
+ $self->{errorcode} = ERR_DECRYPTION_FAILED;
+ return 0;
+ }
+ # add unencrypted payload
+ $payload .= substr($applicationlayer, $offset+$encrypted_length);
+ #printf("decrypted payload %s\n", unpack("H*", $payload));
+ if (unpack('n', $payload) == 0x2f2f) {
+ $self->{decrypted} = 1;
+ } else {
+ # Decryption verification failed
+ $self->{errormsg} = sprintf('Decryption mode %d failed, wrong key?', $self->{cw_parts}{mode});
+ $self->{errorcode} = ERR_DECRYPTION_FAILED;
+ #printf("%x\n", unpack('n', $payload));
+ return 0;
+ }
+ } else {
+ $self->{errormsg} = 'Crypt::Mode::CBC is not installed, please install it (sudo cpan -i Crypt::Mode::CBC)';
+ $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED;
+ return 0;
+ }
+ } else {
+ $self->{errormsg} = 'encrypted message and no aeskey provided';
+ $self->{errorcode} = ERR_NO_AESKEY;
+ return 0;
+ }
+ } else {
+ # error, encryption mode not implemented
+ $self->{errormsg} = sprintf('Encryption mode %x not implemented', $self->{cw_parts}{mode});
+ $self->{errorcode} = ERR_UNKNOWN_ENCRYPTION;
+ $self->{isEncrypted} = 1;
+ $self->{decrypted} = 0;
+ return 0;
+ }
+
+ if ($self->{cifield} == CI_RESP_SML_4 || $self->{cifield} == CI_RESP_SML_12) {
+ # payload is SML encoded, that's not implemented
+ $self->{errormsg} = "payload is SML encoded, can't be decoded, SML payload is " . unpack("H*", substr($applicationlayer,$offset));
+ $self->{errorcode} = ERR_SML_PAYLOAD;
+ return 0;
+ } else {
+ return $self->decodePayload($payload);
+ }
+
+}
+
+sub decodeLinkLayer($$)
+{
+ my $self = shift;
+ my $linklayer = shift;
+ #print "decodeLinkLayer\n";
+
+ if (length($linklayer) < TL_BLOCK_SIZE + $self->{crc_size}) {
+ $self->{errormsg} = "link layer too short";
+ $self->{errorcode} = ERR_LINK_LAYER_INVALID;
+ return 0;
+ }
+ ($self->{lfield}, $self->{cfield}, $self->{mfield}) = unpack('CCv', $linklayer);
+ $self->{afield} = substr($linklayer,4,6);
+ $self->{afield_identno} = substr($self->{afield}, 0, 4);
+ $self->{afield_id} = sprintf("%08d", $self->decodeBCD(8,substr($linklayer,4,4)));
+ ($self->{afield_ver}, $self->{afield_type}) = unpack('CC', substr($linklayer,8,2));
+
+ #printf("lfield %d\n", $self->{lfield});
+
+ if ($self->{frame_type} eq FRAME_TYPE_A) {
+ #print "FRAME TYPE A\n";
+ if ($self->{crc_size} > 0) {
+ $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size}));
+
+ #printf("crc0 %x calc %x\n", $self->{crc0}, $self->calcCRC(substr($linklayer,0,10)));
+
+ if ($self->{crc0} != $self->calcCRC(substr($linklayer,0,TL_BLOCK_SIZE))) {
+ $self->{errormsg} = "CRC check failed on link layer";
+ $self->{errorcode} = ERR_CRC_FAILED;
+ #print "CRC check failed on link layer\n";
+ return 0;
+ }
+ }
+
+ # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller
+ $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself
+ $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE);
+ $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0;
+ $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size};
+
+ #printf("calc len %d, actual %d crc_size %d\n", $self->{msglen}, length($self->{msg}), $self->{crc_size});
+ $self->{applicationlayer} = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size}));
+ return 0 if $self->{errorcode};
+
+ } else {
+ # FRAME TYPE B
+ # each block is at most 129 bytes long.
+ # first contains the header (TL_BLOCK), L field and trailing crc
+ # L field is included in crc calculation
+ # each following block contains only data and trailing crc
+ #print "FRAME TYPE B\n";
+ if (length($self->{msg}) < $self->{lfield}) {
+ $self->{errormsg} = "message too short, expected " . $self->{lfield} . ", got " . length($self->{msg}) . " bytes";
+ $self->{errorcode} = ERR_MSG_TOO_SHORT;
+ return 0;
+ }
+
+
+ my $length = 129;
+ if ($self->{lfield} < $length) {
+ $length = $self->{lfield};
+ }
+ if ($self->{crc_size} > 0) {
+ $length -= $self->{crc_size};
+ $length++; # for L field
+ #print "length: $length\n";
+ $self->{crc0} = unpack('n', substr($self->{msg}, $length, $self->{crc_size}));
+
+ #printf "crc in msg %x crc calculated %x\n", $self->{crc0}, $self->calcCRC(substr($self->{msg}, 0, $length));
+ if ($self->{crc0} != $self->calcCRC(substr($self->{msg}, 0, $length))) {
+ $self->{errormsg} = "CRC check failed on block 1";
+ $self->{errorcode} = ERR_CRC_FAILED;
+ return 0;
+ }
+ }
+
+ $self->{datablocks} = int($self->{lfield} / 129);
+ $self->{datablocks}++ if $self->{lfield} % 129 != 0;
+ # header block is 10 bytes, following block
+ $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1) - ($self->{datablocks} * $self->{crc_size}) ; # this is with CRCs but without the lfield itself
+ $self->{msglen} = $self->{lfield};
+
+ if ($self->{datablocks} == 2) {
+ # TODO
+ } else {
+ $self->{applicationlayer} = substr($self->{msg}, TL_BLOCK_SIZE, $length - TL_BLOCK_SIZE); # - $self->{crc_size});
+ }
+ }
+
+ if (length($self->{msg}) > $self->{msglen}) {
+ $self->{remainingData} = substr($self->{msg},$self->{msglen});
+ } elsif (length($self->{msg}) < $self->{msglen}) {
+ $self->{errormsg} = "message too short, expected " . $self->{msglen} . ", got " . length($self->{msg}) . " bytes";
+ $self->{errorcode} = ERR_MSG_TOO_SHORT;
+ return 0;
+ }
+
+
+ # according to the MBus spec only upper case letters are allowed.
+ # some devices send lower case letters none the less
+ # convert to upper case to make them spec conformant
+ $self->{manufacturer} = uc($self->manId2ascii($self->{mfield}));
+ $self->{typestring} = $validDeviceTypes{$self->{afield_type}} || 'unknown';
+ return 1;
+}
+
+sub encodeLinkLayer($)
+{
+ my $self = shift;
+
+ my $linklayer = pack('CCv', $self->{lfield}, $self->{cfield}, $self->{mfield});
+ ($self->{lfield}, $self->{cfield}, $self->{mfield}) = unpack('CCv', $linklayer);
+ $self->{afield} = substr($linklayer,4,6);
+ $self->{afield_id} = sprintf("%08d", $self->decodeBCD(8,substr($linklayer,4,4)));
+ ($self->{afield_ver}, $self->{afield_type}) = unpack('CC', substr($linklayer,8,2));
+
+ #printf("lfield %d\n", $self->{lfield});
+
+ if ($self->{frame_type} eq FRAME_TYPE_A) {
+ if ($self->{crc_size} > 0) {
+ $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size}));
+
+ #printf("crc0 %x calc %x\n", $self->{crc0}, $self->calcCRC(substr($linklayer,0,10)));
+
+ if ($self->{crc0} != $self->calcCRC(substr($linklayer,0,TL_BLOCK_SIZE))) {
+ $self->{errormsg} = "CRC check failed on link layer";
+ $self->{errorcode} = ERR_CRC_FAILED;
+ #print "CRC check failed on link layer\n";
+ return 0;
+ }
+ }
+
+ # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller
+ $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself
+ $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE);
+ $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0;
+ $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size};
+
+ #printf("calc len %d, actual %d\n", $self->{msglen}, length($self->{msg}));
+ $self->{applicationlayer} = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size}));
+
+ } else {
+ # FRAME TYPE B
+ # each block is at most 129 bytes long.
+ # first contains the header (TL_BLOCK), L field and trailing crc
+ # L field is included in crc calculation
+ # each following block contains only data and trailing crc
+ if (length($self->{msg}) < $self->{lfield}) {
+ $self->{errormsg} = "message too short, expected " . $self->{lfield} . ", got " . length($self->{msg}) . " bytes";
+ $self->{errorcode} = ERR_MSG_TOO_SHORT;
+ return 0;
+ }
+
+
+ my $length = 129;
+ if ($self->{lfield} < $length) {
+ $length = $self->{lfield};
+ }
+ if ($self->{crc_size} > 0) {
+ $length -= $self->{crc_size};
+ $length++; # for L field
+ #print "length: $length\n";
+ $self->{crc0} = unpack('n', substr($self->{msg}, $length, $self->{crc_size}));
+
+ #printf "crc in msg %x crc calculated %x\n", $self->{crc0}, $self->calcCRC(substr($self->{msg}, 0, $length));
+ if ($self->{crc0} != $self->calcCRC(substr($self->{msg}, 0, $length))) {
+ $self->{errormsg} = "CRC check failed on block 1";
+ $self->{errorcode} = ERR_CRC_FAILED;
+ return 0;
+ }
+ }
+
+ $self->{datablocks} = int($self->{lfield} / 129);
+ $self->{datablocks}++ if $self->{lfield} % 129 != 0;
+ # header block is 10 bytes, following block
+ $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1) - ($self->{datablocks} * $self->{crc_size}) ; # this is with CRCs but without the lfield itself
+ $self->{msglen} = $self->{lfield};
+
+ if ($self->{datablocks} == 2) {
+ # TODO
+ } else {
+ $self->{applicationlayer} = substr($self->{msg}, TL_BLOCK_SIZE, $length - TL_BLOCK_SIZE); # - $self->{crc_size});
+ }
+ }
+
+ if (length($self->{msg}) > $self->{msglen}) {
+ $self->{remainingData} = substr($self->{msg},$self->{msglen});
+ } elsif (length($self->{msg}) < $self->{msglen}) {
+ $self->{errormsg} = "message too short, expected " . $self->{msglen} . ", got " . length($self->{msg}) . " bytes";
+ $self->{errorcode} = ERR_MSG_TOO_SHORT;
+ return 0;
+ }
+
+
+ # according to the MBus spec only upper case letters are allowed.
+ # some devices send lower case letters none the less
+ # convert to upper case to make them spec conformant
+ $self->{manufacturer} = uc($self->manId2ascii($self->{mfield}));
+ $self->{typestring} = $validDeviceTypes{$self->{afield_type}} || 'unknown';
+ return 1;
+}
+
+
+sub setFrameType($$)
+{
+ my $self = shift;
+ $self->{frame_type} = shift;
+}
+
+sub getFrameType($)
+{
+ my $self = shift;
+ return $self->{frame_type};
+}
+
+sub parse($$)
+{
+ my $self = shift;
+ $self->{msg} = shift;
+
+ $self->{errormsg} = '';
+ $self->{errorcode} = ERR_NO_ERROR;
+
+ if (length($self->{msg}) < 12) {
+ $self->{errormsg} = "Message too short";
+ $self->{errorcode} = ERR_MSG_TOO_SHORT;
+ return 1;
+ }
+ if (substr($self->{msg}, 0, 4) eq pack("H*", "543D543D")) {
+ $self->setFrameType(FRAME_TYPE_B);
+ $self->{msg} = substr($self->{msg},4);
+ }
+ 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();
+}
+
+sub dumpResult($)
+{
+ my $self = shift;
+
+ if ($self->{linkLayerOk}) {
+ printf("Manufacturer %x %s\n", $self->{mfield}, $self->{manufacturer});
+ printf("IdentNumber %s\n", $self->{afield_id});
+ printf("Version %d\n", $self->{afield_ver});
+ printf("Type %x %s\n", $self->{afield_type}, $self->{typestring});
+ printf("IsEncrypted %d\n", $self->{isEncrypted});
+
+ printf("Status: %x %s\n", $self->{status}, $self->{statusstring});
+ if ($self->{cw_parts}{mode} == 5) {
+ print "Codeword:\n";
+ print "bidirectional: ". $self->{cw_parts}{bidirectional} . "\n";
+ print "accessability: ". $self->{cw_parts}{accessability} . "\n";
+ print "synchronous: $self->{cw_parts}{synchronous}\n";
+ print "mode: $self->{cw_parts}{mode}\n";
+ print "encrypted_blocks: $self->{cw_parts}{encrypted_blocks}\n";
+ print "content: $self->{cw_parts}{content}\n";
+ print "hops: $self->{cw_parts}{hops}\n";
+ }
+ }
+
+ if ($self->{errorcode} == ERR_NO_ERROR) {
+ if ($self->{cifield} == CI_RESP_12) {
+ printf("Meter Id %d\n", $self->{meter_id});
+ printf("Meter Manufacturer %x %s\n", $self->{meter_man}, $self->manId2ascii($self->{meter_man}));
+ printf("Meter Version %d\n", $self->{meter_vers});
+ printf("Meter Dev %x %s\n", $self->{meter_dev}, $self->type2string($self->{meter_dev}));
+ printf("Access No %d\n", $self->{access_no});
+ printf("Status %x\n", $self->{status});
+ }
+
+ my $dataBlocks = $self->{datablocks};
+ my $dataBlock;
+
+ for $dataBlock ( @$dataBlocks ) {
+ #if ( $dataBlock->{type} eq "MANUFACTURER SPECIFIC") {
+ # print $dataBlock->{number} . " " . $dataBlock->{type} . "\n";
+ #} else {
+ print $dataBlock->{number} . ". StorageNo " . $dataBlock->{storageNo} . " " ;
+ print $dataBlock->{functionFieldText} . " ";
+ print $dataBlock->{type} . " " . $dataBlock->{value} . " " . $dataBlock->{unit};
+ if ($dataBlock->{errormsg}) {
+ print "(" . $dataBlock->{errormsg} . ")";
+ }
+ if (defined($dataBlock->{extension_unit})) {
+ print " [" . $dataBlock->{extension_unit} . ", " . $dataBlock->{extension_value} . "]";
+ }
+ print "\n";
+ #}
+ }
+ } else {
+ printf("Error %d: %s\n", $self->{errorcode}, $self->{errormsg});
+ }
+}
+
+1;