UNPKG

iobroker.wireless-mbus

Version:

Receive data from Wireless Meter-Bus (wM-Bus) devices like gas or electricity meters

1,183 lines (1,062 loc) 69 kB
/* * # vim: tabstop=4 shiftwidth=4 expandtab * * This work is part of the ioBroker wmbus adapter * and is licensed under the terms of the GPL2 license. * Copyright (C) 2019 ISFH * * ported from FHEM WMBus.pm # $Id: WMBus.pm 8659 2015-05-30 14:41:28Z kaihs $ * http://www.fhemwiki.de/wiki/WMBUS * extended by soef * * 'partially re-ported' at 2019-Jan-04 by Christian Landvogt * git-svn-id: https://svn.fhem.de/fhem/trunk@18058 2b470e98-0d58-463d-a4d8-8e2adae1ed80 * * many bugfixes, refactoring and additional features by Christian Landvogt * */ const crypto = require('crypto'); const aesCmac = require('node-aes-cmac').aesCmac; const VIFInfo = require('./vifinfo.js'); let tchDecoder; try { tchDecoder = require('./tch-decoder.js'); } catch (ex) { tchDecoder = function() { return false; }; } let priosDecoder; try { priosDecoder = require('./prios-decoder.js'); } catch (ex) { priosDecoder = function() { return false; }; } class CRC { constructor(polynom, initValue, finalXor) { this.polynom = (typeof polynom !== 'undefined' ? polynom : 0x3D65); this.initValue = (typeof initValue !== 'undefined' ? initValue : 0); this.finalXor = (typeof finalXor !== 'undefined' ? finalXor : 0xFFFF); this.table = []; for (let i = 0; i < 256; i++) { let r = i << 8; for (let j = 0; j < 8; j++) { if (r & (1 << 15)) { r = (r << 1) ^ this.polynom; } else { r = (r << 1); } } this.table[i] = r; } } calc(data) { if (!Buffer.isBuffer(data)) { data = Buffer.from(data); } const that = this; let chk = this.initValue; data.forEach(function(val) { chk = that.table[((chk >> 8) ^ val) & 0xFF] ^ (chk << 8); }); chk ^= this.finalXor; chk &= 0xFFFF; return chk; } } class WMBUS_DECODER { constructor(logger, drCache) { this.logger = {}; if (typeof logger === 'undefined') { this.logger.debug = console.log; this.logger.error = console.log; } else if (typeof logger === 'function') { this.logger.debug = logger; this.logger.error = logger; } else { this.logger.debug = (typeof logger.debug === 'function' ? logger.debug : function() {}); this.logger.error = (typeof logger.error === 'function' ? logger.error : function() {}); } this.crc = new CRC(); this.drCache = []; this.constant = { // Data Link Layer DLL_SIZE: 10, // block size FRAME_A_BLOCK_SIZE: 16, FRAME_B_BLOCK_SIZE: 128, AES_BLOCK_SIZE: 16, // 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 // 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_2: 0x8c, // Extended Link Layer, 2 Bytes - OMS CI_ELL_8: 0x8d, // Extended Link Layer, 8 Bytes CI_ELL_10: 0x8e, // Extended Link Layer, 10 Bytes - OMS CI_ELL_16: 0x8f, // Extended Link Layer, 16 Bytes 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 // 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', FRAME_TYPE_WIRED: 'W', }; // see 4.2.3, page 24 this.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)', 0x43: 'Heat meter (TCH)', 0x62: 'Hot water meter (TCH)', 0x72: 'Cold water meter (TCH)', 0x80: 'Heat cost allocator (TCH)', 0xF0: 'Smoke detector (TCH)', }; // bitfield, errors can be combined, see 4.2.3.2 on page 22 this.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', }; this.encryptionModes = { 0x00: 'standard unsigned', 0x01: 'signed data telegram', 0x02: 'static telegram (DES)', 0x03: 'reserved (DES?)', 0x04: 'AES128-CBC static initialisation vector', 0x05: '(OMS) AES128-CBC persistent symmetric key', 0x06: 'reserved', 0x07: '(OMS) AES128-CBC ephemeral symmetric key', 0x08: 'reserved', 0x09: 'reserved', 0x0A: 'reserved', 0x0B: 'reserved', 0x0C: 'reserved', 0x0D: '(OMS) Asymetric encryption using TLS' }; this.functionFieldTypes = { 0b00: 'Instantaneous value', 0b01: 'Maximum value', 0b10: 'Minimum value', 0b11: 'Value during error state', }; this.errorCode = this.constant.ERR_NO_ERROR; this.errorMessage = ''; this.frame_type = this.constant.FRAME_TYPE_A; // default this.alreadyDecrypted = false; this.enableDataRecordCache = (typeof drCache !== 'undefined' ? drCache : false); } // constructor end formatDate(date, format) { function pad(num) { return num < 10 ? '0' + num : '' + num; } let s = format.replace('YYYY', date.getFullYear()); s = s.replace('MM', pad(date.getMonth()+1)); s = s.replace('DD', pad(date.getDate())); s = s.replace('hh', pad(date.getHours())); s = s.replace('mm', pad(date.getMinutes())); s = s.replace('ss', pad(date.getSeconds())); return s; } valueCalcNumeric(value, VIB) { let num = value * VIB.valueFactor; if (VIB.valueFactor < 1 && num.toFixed(0) != num) { num = num.toFixed(VIB.valueFactor.toString().length - 2); } return num; } valueCalcDate(value, VIB) { // eslint-disable-line no-unused-vars //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 const day = (value & 0b11111); const month = ((value & 0b111100000000) >> 8); const year = (((value & 0b1111000000000000) >> 9) | ((value & 0b11100000) >> 5)) + 2000; if (day > 31 || month > 12) { this.logger.debug('invalid date: ' + value); //return "invalid: " + value; } const date = new Date(year, month-1, day); return this.formatDate(date, 'YYYY-MM-DD'); } valueCalcDateTimeTypeI(value, VIB) { // eslint-disable-line no-unused-vars const buf = Buffer.alloc(6); buf.writeUIntLE(value, 0, 6); const seconds = buf[0] & 0x3F; const minutes = buf[1] & 0x3F; const hours = buf[2] & 0x1F; const day = buf[3] & 0x1F; const month = buf[4] & 0x0F; const year = ((buf[3] & 0xE0) >> 5) | ((buf[4] & 0xF0) >> 1); const date = new Date(2000 + year, month - 1, day, hours, minutes, seconds); return this.formatDate(date, 'YYYY-MM-DD hh:mm:ss'); } valueCalcDateTime(value, VIB) { if (value > 0xFFFFFFFF) { return this.valueCalcDateTimeTypeI(value, VIB); } // 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> const datePart = value >> 16; const timeInvalid = value & 0b10000000; let dateTime = this.valueCalcDate(datePart, VIB); if (timeInvalid == 0) { const min = (value & 0b111111); const hour = (value >> 8) & 0b11111; const su = (value & 0b1000000000000000); if (min > 59 || hour > 23) { dateTime = 'invalid: ' + value; } else { const date = new Date(0); date.setHours(hour); date.setMinutes(min); dateTime += ' ' + this.formatDate(date, 'hh:mm') + (su ? ' DST' : ''); } } return dateTime; } valueCalcHex(value, VIB) { // eslint-disable-line no-unused-vars return value.toString(16); } valueCorrectionAdd(ext, VIB) { const exponent = ext.vif & ext.info.expMask; const value = Math.pow(10, exponent + ext.info.bias); VIB.value += value; } valueCorrectionMult(ext, VIB) { const exponent = ext.vif & ext.info.expMask; const value = Math.pow(10, exponent + ext.info.bias); VIB.value *= value; if (value < 1 && VIB.value.toFixed(0) != VIB.value) { VIB.value = VIB.value.toFixed(value.toString().length - 2); } } valueExtDescription(ext, VIB) { VIB.description += (VIB.description.length ? '; ' : '') + ext.info.unit; } valueExtUnit(ext, VIB) { VIB.unit += (VIB.unit.length ? ' ' : '') + ext.info.unit; } valueDurationDescription(ext, VIB) { const value = (ext.vif & ext.info.expMask) + ext.info.bias; VIB.description += (VIB.description.length ? '; ' : '') + ext.info.unit + ': ' + value.toString(); } valueCalcTimeperiod(value, VIB) { switch (VIB.exponent) { case 0: VIB.unit = 's'; break; case 1: VIB.unit = 'min'; break; case 2: VIB.unit = 'h'; break; case 3: VIB.unit = 'd'; break; default: VIB.unit = ''; } return value; } valueCalcTimeperiodPP(value, VIB) { switch (VIB.exponent) { case 0: VIB.unit = 'h'; break; case 1: VIB.unit = 'd'; break; case 2: VIB.unit = 'months'; break; case 3: VIB.unit = 'years'; break; default: VIB.unit = ''; } return value; } valueCalcMap(type) { switch (type) { case 'numeric': return this.valueCalcNumeric; case 'date': return this.valueCalcDate; case 'datetime': return this.valueCalcDateTime; case 'hex': return this.valueCalcHex; case 'timeperiod': return this.valueCalcTimeperiod; case 'timeperiodPP': return this.valueCalcTimeperiodPP; case 'correctionAdd': return this.valueCorrectionAdd; case 'correctionMult': return this.valueCorrectionMult; case 'extendDescription': return this.valueExtDescription; case 'extendUnit': return this.valueExtUnit; case 'duration': return this.valueDurationDescription; default: return ''; } } type2string(type) { return this.validDeviceTypes[type] || 'unknown' ; } state2string(state) { const result = []; if (state) { for (const i in this.validStates) { if (i & state) { result.push(this.validStates[i]); } } } else { result.push(this.validStates[0]); } return result; } manId2hex(idascii) { return (idascii.charCodeAt(0) - 64) << 10 | (idascii.charCodeAt(1) - 64) << 5 | (idascii.charCodeAt(2) - 64); } manId2ascii(idhex) { return String.fromCharCode((idhex >> 10) + 64) + String.fromCharCode(((idhex >> 5) & 0x1f) + 64) + String.fromCharCode((idhex & 0x1f) + 64); } decodeConfigword(cw) { this.config = {}; this.config.mode = (cw & 0b0001111100000000) >> 8; switch (this.config.mode) { case 0: case 5: this.config.bidirectional = (cw & 0b1000000000000000) >> 15; /* mode 5 */ this.config.accessability = (cw & 0b0100000000000000) >> 14; /* mode 5 */ this.config.synchronous = (cw & 0b0010000000000000) >> 13; /* mode 5 */ /* 0b0001111100000000 - mode */ this.config.encrypted_blocks = (cw & 0b0000000011110000) >> 4; /* mode 5 + 7 */ this.config.content = (cw & 0b0000000000001100) >> 2; /* mode 5 */ this.config.hop_counter = (cw & 0b0000000000000011); /* mode 5 */ break; case 7: this.config.content = (cw & 0b1100000000000000) >> 14; /* mode 7 + 13 */ /* 0b0010000000000000 - reserved for counter size */ /* 0b0001111100000000 - mode */ this.config.encrypted_blocks = (cw & 0b0000000011110000) >> 4; /* mode 5 + 7 */ /* 0b0000000000001111 - reserved for counter index */ break; case 13: this.config.content = (cw & 0b1100000000000000) >> 14; /* mode 7 + 13 */ /* 0b0010000000000000 - reserved */ /* 0b0001111100000000 - mode */ this.config.encrypted_bytes = cw & 0b0000000011111111; /* mode 13 */ break; default: this.logger.error('Warning unknown security mode: ' + this.config.mode); } } decodeConfigwordExt(cwe) { if (this.config.mode == 7) { /* 0b10000000 - reserved 0b01000000 - reserved for version */ this.config.kdf_sel = (cwe & 0b00110000) >> 4; this.config.keyid = cwe & 0b00001111; return; } if (this.config.mode == 13) { /* 0b11110000 - reserved */ this.config.proto_type = cwe & 0b00001111; return; } } decodeBCD(digits, bcd) { // check for negative BCD (not allowed according to specs) let sign = 1; if (bcd[digits/2 - 1] >> 4 > 9) { bcd[digits/2 - 1] &= 0b00001111; sign = -1; } let val = 0; for (let i = 0; i < digits / 2; i++) { val += ((bcd[i] & 0x0f) + (((bcd[i] & 0xf0) >> 4) * 10)) * Math.pow(100, i); } return parseInt(sign*val); } decodeValueInformationBlock(data, offset, dataRecord) { function findTabIndex (el) { return (this.vif & this.table[el].typeMask) == this.table[el].type; } function processVIF(vif, info) { VIB.exponent = vif & info.expMask; VIB.unit = info.unit; VIB.description = info.description; if (VIB.type === 'VIF_TYPE_MANUFACTURER_UNKOWN') { VIB.description = '0x' + vif.toString(16) + ' ' + VIB.description; } if ((typeof VIB.exponent !== 'undefined') && (typeof info.bias !== 'undefined')) { VIB.valueFactor = Math.pow(10, (VIB.exponent + info.bias)); } else { VIB.valueFactor = 1; } const func = this.valueCalcMap(info.calcFunc); if (typeof func === 'function') { VIB.calcFunc = func.bind(this); } } let vif; const vifs = []; let vifTable = VIFInfo.primary; let type = 'primary'; const VIB = {}; do { if (vifs.length > 10) { VIB.errorMessage = 'too many VIFE'; VIB.errorCode = this.constant.ERR_TOO_MANY_VIFE; this.logger.error(VIB.errorMessage); // is breaking a good idea? break; } if (offset+1 >= data.length) { this.logger.error('Warning: no data but VIF extension bit still set!'); break; } vif = data[offset++]; if ((vif & 0x7F) == 0x7C) { // plain text vif const len = data[offset++]; vifs.push({ table: null, vif: data.toString('ascii', offset, offset+len).split('').reverse().join(''), type: type + '-plain' }); offset += len; if (vif & 0x80) { continue; } else { break; } } else if (vif == 0xFB) { // just switch table vifTable = VIFInfo.primaryFB; vif = data[offset++]; type += '-FB'; } else if (vif == 0xFD) { // just switch table vifTable = VIFInfo.primaryFD; vif = data[offset++]; type += '-FD'; } else if ((vif == 0xFF) || (vif == 0x7F)) { // manufacturer specific if (vif == 0xFF) { vif = data[offset++]; } if (typeof VIFInfo.manufacturer[this.link_layer.manufacturer] !== 'undefined') { vifTable = VIFInfo.manufacturer[this.link_layer.manufacturer]; type += '-' + this.link_layer.manufacturer; } else { this.logger.debug('Unknown manufacturer specific vif: 0x' + vif.toString(16)); vifTable = VIFInfo.unknown; } } vifs.push({ table: vifTable, vif: vif & 0x7F, type: type }); type = 'extension'; vifTable = VIFInfo.extension; } while (vif & 0x80); VIB.ext = []; vifs.forEach(function (item) { if (item.type.startsWith('primary')) { // primary if (item.type.endsWith('plain')) { VIB.type = 'VIF_PLAIN_TEXT'; VIB.unit = item.vif; } else { const tabIndex = Object.keys(item.table).findIndex(findTabIndex, item); if (tabIndex === -1) { // not found VIB.errorMessage = 'unknown ' + item.type + ' VIF 0x' + item.vif.toString(16); VIB.type = 'VIF' + item.type.replace('primary', '') + ' 0x' + item.vif.toString(16); VIB.errorCode = this.constant.ERR_UNKNOWN_VIFE; } else { VIB.type = Object.keys(item.table)[tabIndex]; processVIF.call(this, item.vif, item.table[Object.keys(item.table)[tabIndex]]); } } } else { // extension if (typeof item.table !== 'undefined') { if (item.type.endsWith('plain')) { item.unit = item.vif; } else { const tabIndex = Object.keys(item.table).findIndex(findTabIndex, item); if (tabIndex === -1) { // not found VIB.errorMessage = 'unknown ' + item.type + ' VIFExt 0x' + item.vif.toString(16); VIB.errorCode = this.constant.ERR_UNKNOWN_VIFE; } else { item.info = item.table[Object.keys(item.table)[tabIndex]]; item.type = Object.keys(item.table)[tabIndex]; delete item.table; } } } VIB.ext.push(item); } }.bind(this)); //this.logger.debug("VIB"); //this.logger.debug(VIB); dataRecord.VIB = VIB; return offset; } decodeDataInformationBlock(data, offset, dataRecord) { let dif = data[offset++]; let difExtNo = 0; const DIB = {}; DIB.tariff = 0; DIB.devUnit = 0; DIB.storageNo = (dif & 0b01000000) >> 6; DIB.functionField = (dif & 0b00110000) >> 4; DIB.dataField = dif & 0b00001111; DIB.functionFieldText = this.functionFieldTypes[DIB.functionField]; while (dif & this.constant.DIF_EXTENSION_BIT) { if (offset >= data.length) { this.logger.error('Warning: no data but DIF extension bit still set!'); break; } dif = data[offset++]; if (difExtNo > 9) { DIB.errorMessage = 'too many DIFE'; DIB.errorCode = this.constant.ERR_TOO_MANY_DIFE; this.logger.error(DIB.errorMessage); break; } DIB.storageNo |= (dif & 0b00001111) << (difExtNo * 4) + 1; DIB.tariff |= ((dif & 0b00110000 >> 4)) << (difExtNo * 2); DIB.devUnit |= ((dif & 0b01000000 >> 6)) << difExtNo; difExtNo++; } //this.logger.debug("DIB"); //this.logger.debug(DIB); dataRecord.DIB = DIB; return offset; } decodeDataRecords(data) { const use_cache = (this.application_layer.format_signature ? true : false); let offset = 0; let dataRecord; let drCount = 1; let value; this.dataRecords = []; let crcBuffer = Buffer.alloc(0); let drStart; if (use_cache) { this.logger.debug('Using data record cache'); } DataLoop: while (offset < data.length) { while (data[offset] == this.constant.DIF_IDLE_FILLER) { offset++; if (offset >= data.length) { break DataLoop; } } if (!use_cache) { dataRecord = {}; drStart = offset; offset = this.decodeDataInformationBlock(data, offset, dataRecord); if (dataRecord.DIB.dataField == this.constant.DIF_SPECIAL) { if (offset < data.length) { this.logger.debug('DIF_SPECIAL at ' + offset + ': '); this.logger.debug(data.toString('hex', offset)); } break DataLoop; } offset = this.decodeValueInformationBlock(data, offset, dataRecord); crcBuffer = Buffer.concat([crcBuffer, data.slice(drStart, offset)]); } else { dataRecord = this.drCache[this.application_layer.full_frame_payload_index].record[drCount-1]; } try { this.logger.debug(`DIB dataField ${dataRecord.DIB.dataField}`); switch (dataRecord.DIB.dataField) { case this.constant.DIF_NONE: value = ''; offset++; this.logger.debug('DIF_NONE found!'); break; case this.constant.DIF_READOUT: value = ''; offset++; this.logger.debug('DIF_READOUT found!'); break; case this.constant.DIF_BCD2: value = this.decodeBCD(2, data.slice(offset, offset+1)); offset += 1; break; case this.constant.DIF_BCD4: value = this.decodeBCD(4, data.slice(offset, offset+2)); offset += 2; break; case this.constant.DIF_BCD6: value = this.decodeBCD(6, data.slice(offset, offset+3)); offset += 3; break; case this.constant.DIF_BCD8: value = this.decodeBCD(8, data.slice(offset, offset+4)); offset += 4; break; case this.constant.DIF_BCD12: value = this.decodeBCD(12, data.slice(offset, offset+6)); offset += 6; break; case this.constant.DIF_INT8: value = data.readInt8(offset); offset += 1; break; case this.constant.DIF_INT16: value = data.readUInt16LE(offset); offset += 2; break; case this.constant.DIF_INT24: value = data.readUIntLE(offset, 3); offset += 3; break; case this.constant.DIF_INT32: value = data.readUInt32LE(offset); offset += 4; break; case this.constant.DIF_INT48: value = data.readUIntLE(offset, 6); offset += 6; break; case this.constant.DIF_INT64: value = data.readBigUInt64LE(offset).toString(); offset += 8; break; case this.constant.DIF_FLOAT32: // correct? value = data.readFloatLE(offset); offset += 4; break; case this.constant.DIF_VARLEN: let lvar = data[offset++]; // eslint-disable-line no-case-declarations if (lvar <= 0xBF) { if (this.constant[dataRecord.VIB.type] === this.constant.VIF_TYPE_MANUFACTURER_SPECIFIC) { // get as hex string value = data.toString('hex', offset, offset+lvar); } else { // ASCII string with lvar characters value = data.toString('ascii', offset, offset+lvar).split('').reverse().join(''); } offset += lvar; } else if ((lvar >= 0xC0) && (lvar <= 0xCF)) { lvar -= 0xC0; // positive BCD number with (lvar - C0h) * 2 digits value = this.decodeBCD(lvar * 2, data.slice(offset, offset+lvar)); offset += lvar; } else if ((lvar >= 0xD0) && (lvar <= 0xDF)) { lvar -= 0xD0; // negative BCD number with (lvar - D0h) * 2 digits value = -1 * this.decodeBCD(lvar * 2, data.slice(offset, offset+lvar)); offset += lvar; } else if ((lvar >= 0xE0) && (lvar <= 0xEF)) { lvar -= 0xE0; // binary number (lvar - E0h) bytes if (lvar <= 6) { value = data.readUIntLE(offset, lvar); } else { value = data.toString('hex', offset, offset+lvar); } offset += lvar; break; } else if ((lvar >= 0xF0) && (lvar <= 0xFA)) { // floating point number with (lvar - F0h) bytes [to be defined] this.errorMessage = 'in datablock ' + drCount + ': unhandled LVAR field 0x' + lvar.toString(16) + ' - floating point number?'; this.errorCode = this.constant.ERR_UNKNOWN_LVAR; this.logger.error(this.errorMessage); return 0; } else { this.errorMessage = 'in datablock ' + drCount + ': unhandled LVAR field 0x' + lvar.toString(16); this.errorCode = this.constant.ERR_UNKNOWN_LVAR; this.logger.error(this.errorMessage); return 0; } break; default: this.errorMessage = 'in datablock ' + drCount + ': unhandled datafield 0x' + dataRecord.DIB.dataField.toString(16); this.errorCode = this.constant.ERR_UNKNOWN_DATAFIELD; this.logger.error(this.errorMessage); return 0; } if (typeof dataRecord.VIB.calcFunc === 'function') { dataRecord.VIB.value = dataRecord.VIB.calcFunc(value, dataRecord.VIB); this.logger.debug(dataRecord.VIB.type + ': Value raw ' + value + ' value calc ' + dataRecord.VIB.value); } else if (typeof value !== 'undefined') { dataRecord.VIB.value = value; this.logger.debug(dataRecord.VIB.type + ': Value ' + JSON.stringify(value)); } else { dataRecord.VIB.value = ''; this.logger.debug(dataRecord.VIB.type + ': Empty DataRecord?'); } dataRecord.VIB.ext.forEach(function (ext) { if (typeof ext.info === 'undefined') { this.logger.error('Unknown VIFExt 0x' + ext.vif.toString(16)); return; } const func = this.valueCalcMap(ext.info.calcFunc); if (typeof func === 'function') { func.call(this, ext, dataRecord.VIB); } }.bind(this)); //this.logger.debug(dataRecord); this.dataRecords.push(dataRecord); drCount++; } catch (e) { this.logger.debug(e); this.logger.error('Warning: Not enough data for DIB.dataField type! Incomplete telegram data?'); } } if (this.enableDataRecordCache && !use_cache) { const crc = this.crc.calc(crcBuffer); if (this.drCache.findIndex(function(i) { return i.crc == this; }, crc) === -1) { this.drCache.push({crc: crc, record: this.dataRecords}); } } return 1; } decrypt(encrypted, key, iv, algorithm) { // see 4.2.5.3, page 26 let initVector; if (typeof iv === 'undefined') { initVector = Buffer.concat([Buffer.alloc(2), this.link_layer.afield_raw, Buffer.alloc(8, this.application_layer.access_no)]); if (typeof this.application_layer.meter_id !== 'undefined') { initVector.writeUInt32LE(this.application_layer.meter_id, 2); initVector.writeUInt8(this.application_layer.meter_vers, 6); initVector.writeUInt8(this.application_layer.meter_dev, 7); } if (typeof this.application_layer.meter_man !== 'undefined') { initVector.writeUInt16LE(this.application_layer.meter_man); } else { initVector.writeUInt16LE(this.link_layer.mfield); } } else { initVector = iv; } this.logger.debug('IV: ' + initVector.toString('hex')); algorithm = (typeof algorithm === 'undefined' ? 'aes-128-cbc' : algorithm); const decipher = crypto.createDecipheriv(algorithm, key, initVector); decipher.setAutoPadding(false); const padding = encrypted.length % 16; if (padding) { this.logger.debug('Added padding: ' + padding); const len = encrypted.length; encrypted = Buffer.concat([encrypted, Buffer.alloc(16-padding)]); return Buffer.concat([decipher.update(encrypted), decipher.final()]).slice(0, len); } return Buffer.concat([decipher.update(encrypted), decipher.final()]); } decrypt_mode7(encrypted, key, tpl) { // see 9.2.4, page 59 const initVector = Buffer.alloc(16, 0x00); // KDF let msg = Buffer.alloc(16, 0x07); msg[0] = 0x00; // derivation constant (see. 9.5.3) 00 = Kenc (from meter) 01 = Kmac (from meter) msg.writeUInt32LE(this.afl.mcr, 1); if (typeof this.application_layer.meter_id !== 'undefined') { msg.writeUInt32LE(this.application_layer.meter_id, 5); } else { msg.writeUInt32LE(this.link_layer.afield, 5); } const kenc = aesCmac(key, msg, {returnAsBuffer: true}); this.logger.debug('Kenc: ' + kenc.toString('hex')); // MAC verification - could be skipped... msg[0] = 0x01; // derivation constant const kmac = aesCmac(key, msg, {returnAsBuffer: true}); this.logger.debug('Kmac: ' + kmac.toString('hex')); const len = 5 + (this.afl.fcl_mlp * 2); msg = Buffer.alloc(len); msg[0] = this.afl.mcl; msg.writeUInt32LE(this.afl.mcr, 1); if (this.afl.fcl_mlp) { msg.writeUInt16LE(this.afl.ml, 5); } msg = Buffer.concat([msg, tpl, encrypted]); const mac = aesCmac(kmac, msg, {returnAsBuffer: true}); this.logger.debug('MAC: ' + mac.toString('hex')); if (this.afl.mac.compare(mac.slice(0, 8)) !== 0) { this.logger.debug('Warning: received MAC is incorrect. Corrupted data?'); this.logger.debug('MAC received: ' + this.afl.mac.toString('hex')); } return this.decrypt(encrypted, kenc, initVector, 'aes-128-cbc'); } decodeAFL(data, offset) { // reset afl object this.afl = {}; this.afl.ci = data[offset++]; this.afl.afll = data[offset++]; this.logger.debug('AFL AFLL ' + this.afl.afll); this.afl.fcl = data.readUInt16LE(offset); offset += 2; /* 0b1000000000000000 - reserved */ this.afl.fcl_mf = (this.afl.fcl & 0b0100000000000000) != 0; /* More fragments: 0 last fragment; 1 more following */ this.afl.fcl_mclp = (this.afl.fcl & 0b0010000000000000) != 0; /* Message Control Field present in fragment */ this.afl.fcl_mlp = (this.afl.fcl & 0b0001000000000000) != 0; /* Message Length Field present in fragment */ this.afl.fcl_mcrp = (this.afl.fcl & 0b0000100000000000) != 0; /* Message Counter Field present in fragment */ this.afl.fcl_macp = (this.afl.fcl & 0b0000010000000000) != 0; /* MAC Field present in fragment */ this.afl.fcl_kip = (this.afl.fcl & 0b0000001000000000) != 0; /* Key Information present in fragment */ /* 0b0000000100000000 - reserved */ this.afl.fcl_fid = this.afl.fcl & 0b0000000011111111; /* fragment ID */ if (this.afl.fcl_mclp) { // AFL Message Control Field (AFL.MCL) this.afl.mcl = data[offset++]; /* 0b10000000 - reserved */ this.afl.mcl_mlmp = (this.afl.mcl & 0b01000000) != 0; /* Message Length Field present in message */ this.afl.mcl_mcmp = (this.afl.mcl & 0b00100000) != 0; /* Message Counter Field present in message */ this.afl.mcl_kimp = (this.afl.mcl & 0b00010000) != 0; /* Key Information Field present in message */ this.afl.mcl_at = (this.afl.mcl & 0b00001111); /* Authentication-Type */ } if (this.afl.fcl_kip) { // AFL Key Information Field (AFL.KI) this.afl.ki = data.readUInt16LE(offset); offset += 2; this.afl.ki_key_version = (this.afl.ki & 0b1111111100000000) >> 8; /* 0b0000000011000000 - reserved */ this.afl.ki_kdf_selection = (this.afl.ki & 0b0000000000110000) >> 4; this.afl.ki_key_id = (this.afl.ki & 0b0000000000001111); } if (this.afl.fcl_mcrp) { // AFL Message Counter Field (AFL.MCR) this.afl.mcr = data.readUInt32LE(offset); this.logger.debug('AFL MC ' + this.afl.mcr); offset += 4; } if (this.afl.fcl_macp) { // AFL MAC Field (AFL.MAC) // length of the MAC field depends on AFL.MCL.AT indicated by the AFL.MCL field // currently only AT = 5 is used (AES-CMAC-128 8bytes truncated) let mac_len = 0; if (this.afl.mcl_at == 4) { mac_len = 4; } else if (this.afl.mcl_at == 5) { mac_len = 8; } else if (this.afl.mcl_at == 6) { mac_len = 12; } else if (this.afl.mcl_at == 7) { mac_len = 16; } this.afl.mac = data.slice(offset, offset+mac_len); offset += mac_len; this.logger.debug('AFL MAC ' + this.afl.mac.toString('hex')); } if (this.afl.fcl_mlp) { // AFL Message Length Field (AFL.ML) this.afl.ml = data.readUInt16LE(offset); offset += 2; } return offset; } decodeELL(data, offset) { // reset ell object this.ell = {}; this.ell.ci = data[offset++]; // common to all headers this.ell.communication_control = data[offset++]; this.ell.access_number = data[offset++]; switch (this.ell.ci) { case this.constant.CI_ELL_2: // OMS // nothing more to do here break; case this.constant.CI_ELL_8: // session_number see below // payload CRC is part (encrypted) payload - so deal with it later break; case this.constant.CI_ELL_10: // OMS case this.constant.CI_ELL_16: this.ell.manufacturer = data.readUInt16LE(offset); offset += 2; this.ell.address = data.slice(offset, offset+6); offset += 6; // session_number see below break; default: this.logger.error('Warning: unknown extended link layer CI: 0x' + this.ell.ci.toString(16)); } // a little tested - what happens to CRC is still not clear if ((this.ell.ci === this.constant.CI_ELL_16) || (this.ell.ci === this.constant.CI_ELL_8)){ this.ell.session_number = data.readUInt32LE(offset); offset += 4; // payload CRC is part (encrypted) payload - so deal with it later // parse session number this.ell.session_number_enc = (this.ell.session_number & 0b11100000000000000000000000000000) >> 29; this.ell.session_number_time = (this.ell.session_number & 0b00011111111111111111111111110000) >> 4; this.ell.session_number_session = this.ell.session_number & 0b00000000000000000000000000001111; const isEncrypted = this.ell.session_number_enc != 0; // is this already decrypted? check against CRC const rawCRC = data.readUInt16LE(offset); const rawCRCcalc = this.crc.calc(data.slice(offset+2)); this.logger.debug('crc ' + rawCRC.toString(16) + ', calculated ' + rawCRCcalc.toString(16)); if (rawCRC == rawCRCcalc) { this.logger.debug('ELL encryption found, but data already seems to be decrypted - CRC match'); return offset + 2; } if (isEncrypted) { if (this.aeskey) { // AES IV // M-field, A-field, CC, SN, (00, 0000 vs FN BC ???) const initVector = Buffer.concat([ Buffer.alloc(2), (typeof this.ell.address !== 'undefined' ? this.ell.address : this.link_layer.afield_raw), Buffer.alloc(8) ]); initVector.writeUInt16LE((typeof this.ell.manufacturer !== 'undefined' ? this.ell.manufacturer : this.link_layer.mfield)); initVector[8] = this.ell.communication_control & 0xEF; // reset hop counter initVector.writeUInt32LE(this.ell.session_number, 9); data = this.decrypt(data.slice(offset), this.aeskey, initVector, 'aes-128-ctr'); this.logger.debug('Dec: '+ data.toString('hex')); } else { this.errorMessage = 'encrypted message and no aeskey provided'; this.errorCode = this.constant.ERR_NO_AESKEY; this.logger.error(this.errorMessage); return 0; } this.ell.crc = data.readUInt16LE(0); offset += 2; // PayloadCRC is a cyclic redundancy check covering the remainder of the frame (excluding the CRC fields) // payloadCRC is also encrypted const crc = this.crc.calc(data.slice(2)); if (this.ell.crc != crc) { this.logger.debug('crc ' + this.ell.crc.toString(16) + ', calculated ' + crc.toString(16)); this.errorMessage = 'Payload CRC check failed on ELL' + (isEncrypted ? ', wrong AES key?' : ''); this.errorCode = this.constant.ERR_CRC_FAILED; this.logger.error(this.errorMessage); return 0; } offset = data.slice(2); // skip PayloadCRC } } return offset; } decodeApplicationLayer(data, offset) { // initialize some fields this.application_layer = {}; this.application_layer.status = 0; this.application_layer.statusstring = ''; this.application_layer.access_no = 0; this.config = { mode: 0 }; const appStart = offset; this.application_layer.cifield = data[offset++]; switch (this.application_layer.cifield) { case this.constant.CI_RESP_0: case this.constant.CI_SND_UD_MODE_1: // seems to be okay? // no header - only M-Bus? this.logger.debug('No header'); break; case this.constant.CI_RESP_4: case this.constant.CI_RESP_SML_4: this.logger.debug('Short header'); this.application_layer.access_no = data[offset++]; this.application_layer.status = data[offset++]; this.decodeConfigword(data.readUInt16LE(offset)); offset += 2; if ((this.config.mode == 7) || (this.config.mode == 13)) { this.decodeConfigwordExt(data[offset++]); } break; case this.constant.CI_RESP_12: case this.constant.CI_RESP_SML_12: this.logger.debug('Long header'); this.application_layer.meter_id = data.readUInt32LE(offset); offset += 4; this.application_layer.meter_man = data.readUInt16LE(offset); offset += 2; this.application_layer.meter_vers = data[offset++]; this.application_layer.meter_dev = data[offset++]; this.application_layer.access_no = data[offset++]; this.application_layer.status = data[offset++]; this.decodeConfigword(data.readUInt16LE(offset)); offset += 2; if ((this.config.mode == 7) || (this.config.mode == 13)) { this.decodeConfigwordExt(data[offset++]); } //this.application_layer.meter_id = this.application_layer.meter_id.toString().padStart(8, '0'); this.application_layer.meter_devtypestring = this.validDeviceTypes[this.application_layer.meter_dev] || 'unknown'; this.application_layer.meter_manufacturer = this.manId2ascii(this.application_layer.meter_man).toUpperCase(); break; case 0x79: this.logger.debug('Compact frame header'); this.application_layer.format_signature = data.readUInt16LE(offset); offset += 2; // full frame payload checksum is not checked! this.application_layer.full_frame_payload_crc = data.readUInt16LE(offset); offset += 2; this.application_layer.full_frame_payload_index = this.drCache.findIndex(function(i) { return i.crc == this; }, this.application_layer.format_signature); if (this.application_layer.full_frame_payload_index === -1) { this.errorMessage = 'Unknown compact frame format'; this.errorCode = this.constant.ERR_UNKNOWN_COMPACT_FORMAT; this.logger.error(this.errorMessage); return 0; } break; case 0xA0: case 0xA1: case 0xA2: if (this.link_layer.manufacturer === 'TCH') { this.logger.debug('Trying to decode using TCH specific module'); const tchRet = tchDecoder(data, this.link_layer); if (tchRet !== false) { this.dataRecords = tchRet; return 1; } } else if (this.link_layer.manufacturer === 'DME') { this.logger.debug('Trying to decode using Diehl PRIOS module'); console.log(data.toString('hex')); const priosRet = priosDecoder(data, this.link_layer, this.validDeviceTypes); this.logger.debug(`retVal ${priosRet}`); if (ty