fhem-mirror/FHEM/WMBus.pm
2021-11-01 16:04:05 +00:00

2556 lines
83 KiB
Perl
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# $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 => '',
},
);
# according to MBUS Spec:
# E00x xxxx
# Reserved for object actions (master to slave): see table on page 75
# or for error codes (slave to master): see table on page 74
# but Kamstrup uses it for a value that is called
# "target V1, month"
# or
# "V1 reverse"
# both are volumes, with one is sent depends of the value of the configuration register R
# the value of the register is not part of the WMBUS data but is set at manufacturing
my %VIFInfo_KAM2 = (
VIF_TARGET_OR_REVERSE_VOLUME => { # 10(nnn-6) m3/h 0.001l/h to 10000l/h
typeMask => 0b00010000,
expMask => 0b00000111,
type => 0b00010000,
bias => -6,
unit => 'm³/h',
calcFunc => \&valueCalcNumeric,
},
);
# see 4.2.3, page 24
my %validDeviceTypes = (
0x00 => 'Other',
0x01 => 'Oil',
0x02 => 'Electricity',
0x03 => 'Gas',
0x04 => 'Heat',
0x05 => 'Steam',
0x06 => 'Warm Water (30 °C ... 90 °C)',
0x07 => 'Water',
0x08 => 'Heat Cost Allocator',
0x09 => 'Compressed Air',
0x0a => 'Cooling load meter (Volume measured at return temperature: outlet)',
0x0b => 'Cooling load meter (Volume measured at flow temperature: inlet)',
0x0c => 'Heat (Volume measured at flow temperature: inlet)',
0x0d => 'Heat / Cooling load meter',
0x0e => 'Bus / System component',
0x0f => 'Unknown Medium',
0x10 => 'Reserved for utility meter',
0x11 => 'Reserved for utility meter',
0x12 => 'Reserved for utility meter',
0x13 => 'Reserved for utility meter',
0x14 => 'Calorific value',
0x15 => 'Hot water (> 90 °C)',
0x16 => 'Cold water',
0x17 => 'Dual register (hot/cold) Water meter',
0x18 => 'Pressure',
0x19 => 'A/D Converter',
0x1a => 'Smokedetector',
0x1b => 'Room sensor (e.g. temperature or humidity)',
0x1c => 'Gasdetector',
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;
} elsif ($self->{manufacturer} eq 'KAM') {
#print "KAM\n";
#$dataBlockExt->{value} = $vif;
$vifInfoRef = \%VIFInfo_KAM2;
} else {
$dataBlockExt->{value} = $vif;
$vifInfoRef = \%VIFInfo_other;
}
if (findVIF($vif, $vifInfoRef, $dataBlockExt)) {
# if ($self->{manufacturer} ne 'KAM') {
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;
#printf ("payload is %s\n" , unpack("H*", $payload));
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 have been observed in real telegrams
for my $vif ("13","14","15","16") {
my $vifExt = '9' . substr($vif,1,1);
#print $vifExt;
#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" . "0413" . "523B" . "04${vifExt}3C"))) {
# Info, Volume, Max flow, Target Volume
# convert into full frame
print "CF found\n";
$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
. pack("H*", "04${vifExt}3C") . substr($compact,13,4); # target volume
last;
} elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "02FF20" . "04$vif" . "523B"))) {
# Info, Volume, Max flow, 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*", "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;
} elsif ($self->{format_signature} == $self->calcCRC(pack("H*", "0406" . "04FF07" . "04FF08" . "04$vif" . "043B" . "0259" . "025d" . "04FF22" . "026c" . "4406" . "44$vif" . "426c"))) {
# Energy, Info, Info, volume, volume flow, flow temp, return temp, time point date, energy, volume, time point date
# convert into full frame
$applicationlayer = pack("H*", "0406") . substr($compact, 5, 4) # Energy
. pack("H*", "04FF07") . substr($compact, 9, 4) # Info
. pack("H*", "04FF08") . substr($compact, 13, 4) # Info
. pack("H*", "04$vif") . substr($compact, 17, 4) # volume
. pack("H*", "043b") . substr($compact, 21, 4) # volume flow
. pack("H*", "0259") . substr($compact, 25, 2) # flow temp
. pack("H*", "025d") . substr($compact, 27, 2) # return temp
. pack("H*", "04FF22") . substr($compact, 29, 4) # Info
. pack("H*", "026c") . substr($compact, 33, 2) # time point date
. pack("H*", "4406") . substr($compact, 35, 4) # Energy storage 1
. pack("H*", "44$vif") . substr($compact, 39, 4) # volume storage 1
. pack("H*", "426c") . substr($compact, 43, 2); # time point date
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};
if (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;
}
#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});
}
# 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});
if ($self->{errorcode} == ERR_NO_ERROR) {
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;