UNPKG

wm-bus

Version:
1,523 lines (1,396 loc) 55 kB
/* * * 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 * */ "use strict"; var crypto = require('crypto'); var unpackF = require('./unpack').unpack; var app = { log: { debug: console.log, error: console.log }, formatDate: function(date, format) { function pad(s) { return s.length === 1 ? '0' + s : s; } var s = format.replace('YYYY', date.getYear()); s = s.replace('MM', pad(date.getMonth()+1)); s = s.replace('DD', pad(date.getDay())); s = s.replace('hh', pad(date.getHours())); s = s.replace('mm', pad(date.getMinutes())); return s; } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// var CRC = function () { this.polynom = 0x3D65; this.initValue = 0; this.xor = 0xffff; this.table = []; for (var i = 0; i < 256; i++) { var r = i << 8; for (var j = 0; j < 8; j++) { //noinspection JSBitwiseOperatorUsage if (r & (1 << 15)) { r = (r << 1) ^ this.polynom; } else { r = (r << 1); } } this.table[i] = r; } }; CRC.prototype.build = function (data) { var crc = this.initValue; for (var i = 0; i < data.length; ++i) { var code = data.charCodeAt(i); crc = this.table[((crc >> 8) ^ code) & 0xFF] ^ (crc << 8); } crc ^= this.xor; crc &= 0xffff; return crc; }; var crc = new CRC(); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// const cc = { // 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 // 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_WRONG_AESKEY: 13 }; function valueCalcNumeric(value, dataBlock) { var num = value * dataBlock.valueFactor; if (dataBlock.valueFactor < 1 && num.toFixed(0) != num) { num = num.toFixed(dataBlock.valueFactor.toString().length - 2); } return num; } function valueCalcDate(value, dataBlock) { //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 var day = (value & b('0b11111')); var month = ((value & b('0b111100000000')) >> 8); var year = (((value & b('0b1111000000000000')) >> 9) | ((value & b('0b11100000')) >> 5)) + 2000; if (day > 31 || month > 12 || year > 2099) { app.log.error("invalid: " + value); return "invalid: " + value; } var date = new Date(year, month, day); return app.formatDate(date, "YYYY-MM-DD"); } function valueCalcDateTime(value, dataBlock) { //#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> var datePart = value >> 16; var timeInvalid = value & b('0b10000000'); var dateTime = valueCalcDate(datePart, dataBlock); if (timeInvalid == 0) { var min = (value & b('0b111111')); var hour = (value >> 8) & b('0b11111'); var su = (value & b('0b1000000000000000')); if (min > 59 || hour > 23) { dateTime = 'invalid: ' + value; } else { var date = new Date(0); date.setHours(hour); date.setMinutes(min); dateTime = app.formatDate(date, "hh:mm") + su ? 'DST' : ''; } } return dateTime; } function valueCalcHex(value, dataBlock) { return value.toString(16); } function valueCalcu(value, dataBlock) { //noinspection JSBitwiseOperatorUsage return (value & b('0b00001000') ? 'upper' : 'lower') + ' limit'; } function valueCalcufnn(value, dataBlock) { //noinspection JSBitwiseOperatorUsage var result = (value & b('0b00001000') ? 'upper' : 'lower') + ' limit'; //noinspection JSBitwiseOperatorUsage result += ', ' + (value & b('0b00000100') ? 'first' : 'last'); result += ', duration ' + (value & b('0b11')); return result; } function valueCalcMultCorr1000(value, dataBlock) { dataBlock.value *= 1000; return "correction by factor 1000"; } var TimeSpec = { 0: 's', // seconds 1: 'm', // minutes 2: 'h', // hours 3: 'd' // days }; function valueCalcTimeperiod(value, dataBlock) { dataBlock.unit = TimeSpec[dataBlock.exponent]; return value; } function b(sBin) { return parseInt(sBin.slice(2), 2) } // VIF types (Value Information Field), see page 32 const VIFInfo = { VIF_ENERGY_WATT: { // 10(nnn-3) Wh 0.001Wh to 10000Wh typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00000000'), bias: -3, unit: 'Wh', calcFunc: valueCalcNumeric }, VIF_ENERGY_JOULE: { // 10(nnn) J 0.001kJ to 10000kJ typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00001000'), bias: 0, unit: 'J', calcFunc: valueCalcNumeric }, VIF_VOLUME: { // 10(nnn-6) m3 0.001l to 10000l typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00010000'), bias: -6, unit: 'm³', calcFunc: valueCalcNumeric }, VIF_MASS: { // 10(nnn-3) kg 0.001kg to 10000kg typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00011000'), bias: -3, unit: 'kg', calcFunc: valueCalcNumeric }, VIF_ON_TIME_SEC: { // seconds typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100000'), bias: 0, unit: 'sec', calcFunc: valueCalcNumeric }, VIF_ON_TIME_MIN: { // minutes typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100001'), bias: 0, unit: 'min', calcFunc: valueCalcNumeric }, VIF_ON_TIME_HOURS: { // hours typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100010'), bias: 0, unit: 'hours' }, VIF_ON_TIME_DAYS: { // days typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100011'), bias: 0, unit: 'days' }, VIF_OP_TIME_SEC: { // seconds typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100100'), bias: 0, unit: 'sec' }, VIF_OP_TIME_MIN: { // minutes typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100101'), bias: 0, unit: 'min' }, VIF_OP_TIME_HOURS: { // hours typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100110'), bias: 0, unit: 'hours' }, VIF_OP_TIME_DAYS: { // days typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100111'), bias: 0, unit: 'days' }, VIF_ELECTRIC_POWER: { // 10(nnn-3) W 0.001W to 10000W typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00101000'), bias: -3, unit: 'W', calcFunc: valueCalcNumeric }, VIF_THERMAL_POWER: { // 10(nnn) J/h 0.001kJ/h to 10000kJ/h typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00110000'), bias: 0, unit: 'J/h', calcFunc: valueCalcNumeric }, VIF_VOLUME_FLOW: { // 10(nnn-6) m3/h 0.001l/h to 10000l/h typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b00111000'), bias: -6, unit: 'm³/h', calcFunc: valueCalcNumeric }, VIF_VOLUME_FLOW_EXT1: { // 10(nnn-7) m3/min 0.0001l/min to 10000l/min typeMask: b('b01111000'), expMask: b('0b00000111'), type: b('0b01000000'), bias: -7, unit: 'm³/min', calcFunc: valueCalcNumeric }, VIF_VOLUME_FLOW_EXT2: { // 10(nnn-9) m3/s 0.001ml/s to 10000ml/s typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b01001000'), bias: -9, unit: 'm³/s', calcFunc: valueCalcNumeric }, VIF_MASS_FLOW: { // 10(nnn-3) kg/h 0.001kg/h to 10000kg/h typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b01010000'), bias: -3, unit: 'kg/h', calcFunc: valueCalcNumeric }, VIF_FLOW_TEMP: { // 10(nn-3) °C 0.001°C to 1°C typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b01011000'), bias: -3, unit: '°C', calcFunc: valueCalcNumeric }, VIF_RETURN_TEMP: { // 10(nn-3) °C 0.001°C to 1°C typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b01011100'), bias: -3, unit: '°C', calcFunc: valueCalcNumeric }, VIF_TEMP_DIFF: { // 10(nn-3) K 1mK to 1000mK typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b01100000'), bias: -3, unit: 'mK', calcFunc: valueCalcNumeric }, VIF_EXTERNAL_TEMP: { // 10(nn-3) °C 0.001°C to 1°C typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b01100100'), bias: -3, unit: '°C', calcFunc: valueCalcNumeric }, VIF_PRESSURE: { // 10(nn-3) bar 1mbar to 1000mbar typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b01101000'), bias: -3, unit: 'bar', calcFunc: valueCalcNumeric }, VIF_TIME_POINT_DATE: { // data type G typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01101100'), bias: 0, unit: '', calcFunc: valueCalcDate }, VIF_TIME_POINT_DATE_TIME: { // data type F typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01101101'), bias: 0, unit: '', calcFunc: valueCalcDateTime }, VIF_HCA: { // Unit for Heat Cost Allocator, dimensonless typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01101110'), bias: 0, unit: '', calcFunc: valueCalcNumeric }, VIF_FABRICATION_NO: { // Fabrication No typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01111000'), bias: 0, unit: '', calcFunc: valueCalcNumeric }, VIF_OWNER_NO: { // Eigentumsnummer (used by Easymeter even though the standard allows this only for writing to a slave) typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01111001'), bias: 0, unit: '' }, VIF_AVERAGING_DURATION_SEC: { // seconds typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110000'), bias: 0, unit: 'sec', calcFunc: valueCalcNumeric }, VIF_AVERAGING_DURATION_MIN: { // minutes typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110001'), bias: 0, unit: 'min', calcFunc: valueCalcNumeric }, VIF_AVERAGING_DURATION_HOURS: { // hours typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110010'), bias: 0, unit: 'hours' }, VIF_AVERAGING_DURATION_DAYS: { // days typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110011'), bias: 0, unit: 'days' }, VIF_ACTUALITY_DURATION_SEC: { // seconds typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110100'), bias: 0, unit: 'sec', calcFunc: valueCalcNumeric }, VIF_ACTUALITY_DURATION_MIN: { // minutes typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110101'), bias: 0, unit: 'min', calcFunc: valueCalcNumeric }, VIF_ACTUALITY_DURATION_HOURS: { // hours typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110110'), bias: 0, unit: 'hours' }, VIF_ACTUALITY_DURATION_DAYS: { // days typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110111'), bias: 0, unit: 'days' } }; //# Codes used with extension indicator $FD, see 8.4.4 on page 80 var VIFInfo_FD = { VIF_CREDIT: { // Credit of 10nn-3 of the nominal local legal currency units typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b00000000'), bias: -3, unit: '€', calcFunc: valueCalcNumeric }, VIF_DEBIT: { // Debit of 10nn-3 of the nominal local legal currency units typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b00000100'), bias: -3, unit: '€', calcFunc: valueCalcNumeric }, VIF_ACCESS_NO: { // Access number (transmission count) typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00001000'), bias: 0, unit: '', calcFunc: valueCalcNumeric }, VIF_MEDIUM: { // Medium (as in fixed header) typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00001001'), bias: 0, unit: '', calcFunc: valueCalcNumeric }, VIF_MODEL_VERSION: { // Model / Version typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00001100'), bias: 0, unit: '', calcFunc: valueCalcNumeric }, VIF_ERROR_FLAGS: { // Error flags (binary) typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00010111'), bias: 0, unit: '', calcFunc: valueCalcHex }, VIF_DURATION_SINCE_LAST_READOUT: { // Duration since last readout [sec(s)..day(s)] typeMask: b('0b01111100'), expMask: b('0b00000011'), type: b('0b00101100'), bias: 0, unit: 's', calcFunc: valueCalcTimeperiod }, VIF_VOLTAGE: { // 10nnnn-9 Volts typeMask: b('0b01110000'), expMask: b('0b00001111'), type: b('0b01000000'), bias: -9, unit: 'V', calcFunc: valueCalcNumeric }, VIF_ELECTRICAL_CURRENT: { // 10nnnn-12 Ampere typeMask: b('0b01110000'), expMask: b('0b00001111'), type: b('0b01010000'), bias: -12, unit: 'A', calcFunc: valueCalcNumeric }, VIF_RECEPTION_LEVEL: { // reception level of a received radio device. typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01110001'), bias: 0, unit: 'dBm', calcFunc: valueCalcNumeric }, VIF_FD_RESERVED: { // Reserved typeMask: b('0b01110000'), expMask: b('0b00000000'), type: b('0b01110000'), bias: 0, unit: 'Reserved' } }; // Codes used with extension indicator $FB var VIFInfo_FB = { VIF_ENERGY: { // Energy 10(n-1) MWh 0.1MWh to 1MWh typeMask: b('0b01111110'), expMask: b('0b00000001'), type: b('0b00000000'), bias: -1, unit: 'MWh', calcFunc: valueCalcNumeric } }; // Codes used for an enhancement of VIFs other than $FD and $FB var VIFInfo_other = { VIF_ERROR_NONE: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00000000'), bias: 0, unit: 'No error' }, VIF_TOO_MANY_DIFES: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00000001'), bias: 0, unit: 'Too many DIFEs' }, VIF_ILLEGAL_VIF_GROUP: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00001100'), bias: 0, unit: 'Illegal VIF-Group' }, VIF_PER_SECOND: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100000'), bias: 0, unit: 'per second' }, VIF_PER_MINUTE: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100001'), bias: 0, unit: 'per minute' }, VIF_PER_HOUR: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100010'), bias: 0, unit: 'per hour' }, VIF_PER_DAY: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100011'), bias: 0, unit: 'per day' }, VIF_PER_WEEK: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100100'), bias: 0, unit: 'per week' }, VIF_PER_MONTH: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100101'), bias: 0, unit: 'per month' }, VIF_PER_YEAR: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100110'), bias: 0, unit: 'per year' }, VIF_PER_REVOLUTION: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00100111'), bias: 0, unit: 'per revolution/measurement' }, VIF_PER_INCREMENT_INPUT: { typeMask: b('0b01111110'), expMask: b('0b00000000'), type: b('0b00101000'), bias: 0, unit: 'increment per input pulse on input channnel #', calcFunc: valueCalcNumeric }, VIF_PER_INCREMENT_OUTPUT: { typeMask: b('0b01111110'), expMask: b('0b00000000'), type: b('0b00101010'), bias: 0, unit: 'increment per output pulse on output channnel #', calcFunc: valueCalcNumeric }, VIF_PER_LITER: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00101100'), bias: 0, unit: 'per liter' }, VIF_START_DATE_TIME: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00111001'), bias: 0, unit: 'start date(/time) of' }, VIF_ACCUMULATION_IF_POSITIVE: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b00111011'), bias: 0, unit: 'Accumulation only if positive contribution' }, VIF_DURATION_NO_EXCEEDS: { typeMask: b('0b01110111'), expMask: b('0b00000000'), type: b('0b01000001'), bias: 0, unit: '# of exceeds', calcFunc: valueCalcu }, VIF_DURATION_LIMIT_EXCEEDED: { typeMask: b('0b01110000'), expMask: b('0b00000000'), type: b('0b01010000'), bias: 0, unit: 'duration of limit exceeded', calcFunc: valueCalcufnn }, VIF_MULTIPLICATIVE_CORRECTION_FACTOR: { typeMask: b('0b01111000'), expMask: b('0b00000111'), type: b('0b01110000'), bias: -6, unit: '' }, VIF_MULTIPLICATIVE_CORRECTION_FACTOR_1000: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01111101'), bias: 0, unit: '', calcFunc: valueCalcMultCorr1000 }, VIF_FUTURE_VALUE: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01111110'), bias: 0, unit: '' }, VIF_MANUFACTURER_SPECIFIC: { typeMask: b('0b01111111'), expMask: b('0b00000000'), type: b('0b01111111'), bias: 0, unit: 'manufacturer specific' } }; // For Easymeter (manufacturer specific) var VIFInfo_ESY = { VIF_ELECTRIC_POWER_PHASE_NO: { typeMask: b('0b01111110'), expMask: b('0b00000000'), type: b('0b00101000'), bias: 0, unit: 'phase #', calcFunc: valueCalcNumeric }, VIF_ELECTRIC_POWER_PHASE: { typeMask: b('0b01000000'), expMask: b('0b00000000'), type: b('0b00000000'), bias: -2, unit: 'W', calcFunc: valueCalcNumeric } }; // see 4.2.3, page 24 var 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 var 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' }; //var encryptionModes = { // 0x00: 'standard unsigned', // 0x01: 'signed data telegram', // 0x02: 'static telegram', // 0x03: 'reserved', //}; var encryptionModes = { 0: "No encryption", 1: "AES Counter Mode(AES - CTR)", 5: "AES Cipher Block Chaining Mode(AES - CBC) with dynamicinitialization vector" }; var functionFieldTypes = { 0: 'Instantaneous value', 1: 'Maximum value', 2: 'Minimum value', 3: 'Value during error state' }; function manId2hex(idascii) { return (idascii.charCodeAt(1) - 64) << 10 | (idascii.charCodeAt(2) - 64) << 5 | (idascii.charCodeAt(3) - 64); } function manId2ascii (idhex) { //return String.fromCharCode((idhex >> 10) + 64) + String.fromCharCode(((idhex >> 5) & b('0b00011111')) + 64) + String.fromCharCode((idhex & b('0b00011111')) + 64); return String.fromCharCode((idhex >> 10) + 64) + String.fromCharCode(((idhex >> 5) & 0x1f) + 64) + String.fromCharCode((idhex & 0x1f) + 64); } function decodeBCD (digits, bcd) { var val = 0; for (var i = 0; i < digits / 2; i++) { var byte = bcd.charCodeAt(i); val += ((byte & 0x0f) + (((byte & 0xf0) >> 4) * 10)) * Math.pow(100, i); } return val; } function type2string (type) { return validDeviceTypes[type] || 'unknown'; } function state2string (state) { var result = []; if (state) { for (var i in validStates) { //noinspection JSBitwiseOperatorUsage if (i & state) result.push(validStates[i]); } } else result.push(validStates[0]); return result; } function decNo(no, len) { var s = no.toString(); if (len === undefined) len = 8; return '00000000'.substr(1, len - s.length) + s; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// var WMBUS = function (options) { if(!(this instanceof WMBUS)) return new WMBUS(options); this.msg = ""; this.cw_parts = {}; this.aesKey = {}; this.crcRemoved = false; if (options) { if (options.log) app.log = options.log; if (options.formatDate) app.formatDate = options.formatDate; } this.cc = cc; this.crc_size = cc.CRC_SIZE; }; WMBUS.prototype.setCrcSize = function (size) { this.crc_size = size; } WMBUS.prototype.getCrcSize = function (size) { return this.crc_size; } WMBUS.prototype.unpack = function (format, buf, obj) { if (format.indexOf('/') < 0) return unpackF(format + 'a/', buf).a; if (obj === undefined) obj = this; var ar = unpackF(format, buf); for (var i in ar) { obj[i] = ar[i]; } }; WMBUS.prototype.getAESKey = function (sn) { var ret = this.aesKey.hasOwnProperty('sn' + sn) ? this.aesKey['sn'+sn] : ""; if (ret.length === 32) { ret = new Buffer(ret, 'hex'); } return ret; }; WMBUS.prototype.addAESKey = function (manufacturerId, key) { this.aesKey ['sn'+manufacturerId] = key || ""; }; WMBUS.prototype.hasAESKey = function () { return this.getAESKey(this.afield_id).length > 0; }; WMBUS.prototype.removeCRC = function (msg) { var res = ""; var _crc; var blocksize = cc.LL_BLOCK_SIZE; var blocksize_with_crc = cc.LL_BLOCK_SIZE + this.crc_size; //cc.CRC_SIZE; var crcoffset; var msgLen = this.datalen; // size without CRCs var noOfBlocks = this.datablocks; // total number of data blocks, each with a CRC appended var rest = msgLen % cc.LL_BLOCK_SIZE; // size of the last data block, can be smaller than 16 bytes // each block is 16 bytes + 2 bytes CRC app.log.debug("Length " + msgLen + " # blocks " + noOfBlocks + " remaining " + rest); if (this.crc_size === 0) return msg; //!!!! for (var i = 0; i < noOfBlocks; i++) { crcoffset = blocksize_with_crc * i + cc.LL_BLOCK_SIZE; app.log.debug(i + ': crc offset ' + crcoffset); if (rest > 0 && crcoffset + this.crc_size /*cc.CRC_SIZE*/ > (noOfBlocks - 1) * blocksize_with_crc + rest) { // last block is smaller crcoffset = (noOfBlocks - 1) * blocksize_with_crc + rest; app.log.debug('last crc offset ' + crcoffset); blocksize = msgLen - (i * blocksize); } //crc = ((msg.charCodeAt(crcoffset) & 0xFF) << 8) + (msg.charCodeAt(crcoffset + 1) & 0xFF); _crc = this.unpack('n', msg.substr(crcoffset, this.crc_size/*cc.CRC_SIZE*/)); var __crc = crc.build(msg.substr(blocksize_with_crc * i, blocksize)) app.log.debug(i + ': CRC ' + _crc.toString(16) + ', calc ' + __crc.toString(16) + 'blocksize ' + blocksize); //if (crc != this.checkCRC(msg.substr(blocksize_with_crc * i, blocksize))) { if (_crc !== __crc) { this.errormsg = "crc check failed for block " + i; this.errorcode = cc.ERR_CRC_FAILED; return 0; } res += msg.substr(blocksize_with_crc * i, blocksize); } return res; }; WMBUS.prototype.decodeConfigword = function () { //#if (this.cw_parts.mode == 5) { this.cw_parts.bidirectional = this.cw & 0x8000 >> 15; //b('0b1000000000000000') >> 15; this.cw_parts.accessability = this.cw & 0x4000 >> 14; //b('0b0100000000000000') >> 14; this.cw_parts.synchronous = this.cw & 0x2000 >> 13; //b('0b0010000000000000') >> 13; this.cw_parts.mode = this.cw & 0x0f00 >> 8; //b('0b0000111100000000') >> 8; this.cw_parts.encrypted_blocks = this.cw & 0x00f0 >> 4; //b('0b0000000011110000') >> 4; this.cw_parts.content = this.cw & 0x000c >> 2; //b('0b0000000000001100') >> 2; this.cw_parts.repeated_access = this.cw & 0x0002 >> 1; //b('0b0000000000000010') >> 1; this.cw_parts.hops = this.cw & 0x0001; //b('0b0000000000000001'); //#} else if (this.cw_parts.mode == 7) { //# ToDo: wo kommt das dritte Byte her? //# this.cw_parts.mode = this.cw & b('0b0000111100000000') >> 8; //#} }; function findVIF(vif, vifInfoRef, dataBlockRef) { var bias; if (vifInfoRef !== undefined) { for (var vifType in vifInfoRef) { app.log.debug('vifType ' + vifType + ' VIF ' + vif + ' typeMask ' + vifInfoRef[vifType].typeMask + ' type ' + vifInfoRef[vifType].type); if ((vif & vifInfoRef[vifType].typeMask) == vifInfoRef[vifType].type) { app.log.debug('match vifType ' + vifType); bias = vifInfoRef[vifType].bias; dataBlockRef.exponent = vif & vifInfoRef[vifType].expMask; dataBlockRef.type = vifType; dataBlockRef.unit = vifInfoRef[vifType].unit; if (dataBlockRef.exponent != undefined && bias != undefined) { dataBlockRef.valueFactor = Math.pow(10, (dataBlockRef.exponent + bias)) } else { dataBlockRef.valueFactor = 1; } dataBlockRef.calcFunc = vifInfoRef[vifType].calcFunc; app.log.debug('type ' + dataBlockRef.type + ' bias ' + bias + ' exp ' + dataBlockRef.exponent + ' valueFactor ' + dataBlockRef.valueFactor + ' unit ' + dataBlockRef.unit); return 1; } } app.log.debug("no match!"); return 0; } return 1; }; WMBUS.prototype.decodeValueInformationBlock = function (vib, dataBlockRef) { var offset = 0; var vif; var vifInfoRef; var vifExtension = 0; var vifExtNo = 0; var isExtension; var dataBlockExt; var VIFExtensions = []; var analyzeVIF = 1; dataBlockRef.type = ''; // The unit and multiplier is taken from the table for primary VIF vifInfoRef = VIFInfo; EXTENSION: while (1) { vif = vib.charCodeAt(offset++); isExtension = vif & cc.VIF_EXTENSION_BIT; app.log.debug('vif: ' + vif.toString(16) + ' isExtension ' + isExtension); if (!isExtension) { //noinspection UnnecessaryLabelOnBreakStatementJS break EXTENSION; } vifExtNo++; if (vifExtNo > 10) { dataBlockRef.errormsg = 'too many VIFE'; dataBlockRef.errorcode = cc.ERR_TOO_MANY_VIFE; break; } vifExtension = vif; vif &= ~cc.VIF_EXTENSION_BIT; app.log.debug('vif ohne extension: ' + vif.toString(16)); switch (vif) { case 0x7D: vifInfoRef = VIFInfo_FD; break; case 0x7B: vifInfoRef = VIFInfo_FB; break; case 0x7C: //# Plaintext VIF var vifLength = vib.charCodeAt(offset++); dataBlockRef.type = "see unit"; dataBlockRef.unit = this.unpack('C' + vifLength, vib.substr(offset, vifLength)); offset += vifLength; analyzeVIF = 0; break EXTENSION; case 0x7F: if (this.manufacturer === 'ESY') { // Easymeter vif = vib.charCodeAt(offset++); vifInfoRef = VIFInfo_ESY; } else { // manufacturer specific data, can't be interpreted dataBlockRef.type = "MANUFACTURER SPECIFIC"; dataBlockRef.unit = ""; analyzeVIF = 0; } break EXTENSION; default: // enhancement of VIFs other than $FD and $FB (see page 84ff.) app.log.debug("other extension"); dataBlockExt = {}; if (this.manufacturer === 'ESY') { vifInfoRef = VIFInfo_ESY; dataBlockExt.value = vib.charCodeAt(2) * 100; } else { dataBlockExt.value = vif; vifInfoRef = VIFInfo_other; } if (findVIF(vif, vifInfoRef, dataBlockExt)) { VIFExtensions.push(dataBlockExt); } else { dataBlockRef.type = 'unknown'; dataBlockRef.errormsg = "unknown VIFE " + vifExtension.toString(16) + " at offset " + (offset - 1); dataBlockRef.errorcode = cc.ERR_UNKNOWN_VIFE; } break; } if (!isExtension) break; } if (analyzeVIF) { if (findVIF(vif, vifInfoRef, dataBlockRef) == 0) { dataBlockRef.errormsg = "unknown VIF " + vifExtension.toString(16) + " at offset " + (offset - 1); dataBlockRef.errorcode = cc.ERR_UNKNOWN_VIFE; } } dataBlockRef.VIFExtensions = VIFExtensions; if (dataBlockRef.type === '') { dataBlockRef.type = 'unknown'; dataBlockRef.errormsg = "in VIFExtension " + vifExtension.toString(16) + " unknown VIF " + vif.toString(16); dataBlockRef.errorcode = cc.ERR_UNKNOWN_VIF; } return offset; }; WMBUS.prototype.decrypt = function (encrypted) { // see 4.2.5.3, page 26 var initVector = this.msg.substr(2, 8); var iv_access_no = String.fromCharCode(this.access_no); for (var i = 1; i <= 8; i++) { initVector += iv_access_no; } var self = this; try { var ivBuf = new Buffer (initVector, 'binary'); const decipher = crypto.createDecipheriv ('aes-128-cbc', this.getAESKey (this.afield_id), ivBuf, {}); return decipher.update (encrypted, 'binary', 'binary'); } catch(e) { self.errormsg = e.message; self.errorcode = cc.ERR_WRONG_AESKEY; } return ''; }; function decodeDataInformationBlock (dib, dataBlockRef) { var difExtNo = 0; var dif = dib.charCodeAt(0); var offset = 1; var isExtension = dif & cc.DIF_EXTENSION_BIT; dataBlockRef.tariff = 0; dataBlockRef.devUnit = 0; dataBlockRef.storageNo = (dif & 0x0040) >> 6; // b('0b01000000')) >> 6; dataBlockRef.functionField = (dif & 0x0030) >> 4; //b('0b00110000')) >> 4; dataBlockRef.functionFieldText = functionFieldTypes[dataBlockRef.functionField]; dataBlockRef.dataField = dif & 0x000f; // b('0b00001111'); app.log.debug("dif " + dif.toString(16) + " storage " + dataBlockRef.storageNo); while (isExtension) { dif = dib.charCodeAt(offset); if (dif == undefined) break; offset++; isExtension = dif & cc.DIF_EXTENSION_BIT; difExtNo++; if (difExtNo > 10) { dataBlockRef.errormsg = 'too many DIFE'; dataBlockRef.errorcode = cc.ERR_TOO_MANY_DIFE; //last EXTENSION; break; } dataBlockRef.storageNo |= (dif & 0x000f) << (difExtNo * 4) + 1; //b('0b00001111')) << (difExtNo * 4) + 1; dataBlockRef.tariff |= (dif & 0x0030 >> 4) << ((difExtNo - 1) * 2); //b('0b00110000') >> 4)) << ((difExtNo - 1) * 2); dataBlockRef.devUnit |= (dif & 0x0040 >> 6) << (difExtNo - 1); //(dif & b('0b01000000') >> 6) << (difExtNo - 1); app.log.debug("dife " + dif.toString(16) + " extno " + difExtNo + " storage " + dataBlockRef.storageNo); } app.log.debug("in DIF: datafield " + dataBlockRef.dataField.toString(16)); app.log.debug("offset in dif " + offset); return offset; }; WMBUS.prototype.decodeDataRecordHeader = function (drh, dataBlockRef) { var offset = decodeDataInformationBlock(drh, dataBlockRef); offset += this.decodeValueInformationBlock(drh.substr(offset), dataBlockRef); app.log.debug("in DRH: type " + dataBlockRef.type); return offset; }; WMBUS.prototype.decodePayload = function (payload) { var offset = 0, dataBlockNo = 0; var value; var dataBlocks = []; var dataBlock; PAYLOAD: while (offset < payload.length) { dataBlockNo++; //# create a new anonymous hash reference dataBlock = { number: dataBlockNo, unit: '' }; //dataBlock.number = dataBlockNo; //dataBlock.unit = ''; while (payload.charCodeAt(offset) == 0x2f) { app.log.debug("skipping filler at offset " + offset + ' of ' + payload.length); if (++offset >= payload.length) { break PAYLOAD; } } offset += this.decodeDataRecordHeader(payload.substr(offset), dataBlock); app.log.debug("No. " + dataBlockNo + " type " + dataBlock.dataField.toString(16) + " at offset " + (offset - 1)); switch (dataBlock.dataField) { case cc.DIF_NONE: break; case cc.DIF_READOUT: this.errormsg = "in datablock " + dataBlockNo + ": unexpected DIF_READOUT"; this.errorcode = cc.ERR_UNKNOWN_DATAFIELD; return 0; case cc.DIF_BCD2: value = decodeBCD(2, payload.substr(offset, 1)); offset += 1; break; case cc.DIF_BCD4: value = decodeBCD(4, payload.substr(offset, 2)); offset += 2; break; case cc.DIF_BCD6: value = decodeBCD(6, payload.substr(offset, 3)); offset += 3; break; case cc.DIF_BCD8: value = decodeBCD(8, payload.substr(offset, 4)); offset += 4; break; case cc.DIF_BCD12: value = decodeBCD(12, payload.substr(offset, 6)); offset += 6; break; case cc.DIF_INT8: value = this.unpack('C', payload.substr(offset, 1)); offset += 1; break; case cc.DIF_INT16: value = this.unpack('v', payload.substr(offset, 2)); offset += 2; break; case cc.DIF_INT24: value = this.unpack('V', payload.substr(offset, 3)); // use 32 bit formater with 3 bytes input //var bytes = unpackF('Ca0/Ca1/Ca2/', payload.substr(offset, 3)); //value = bytes['a0'] + (bytes['a1'] << 8) + (bytes['a2'] << 16); // With brackets, same result as above offset += 3; break; case cc.DIF_INT32: value = this.unpack('V', payload.substr(offset, 4)); offset += 4; break; case cc.DIF_INT48: var words = unpackF('va0/va1/va2/', payload.substr(offset, 6)); value = words['a0'] + (words['a1'] << 16) + (words['a2'] << 32); //value = (words['a0'] << 0) + (words['a1'] << 16) + (words['a2'] << 32); offset += 6; break; case cc.DIF_INT64: var longs = unpackF('La0/La1/', payload.substr(offset, 8)); value = longs['a0'] + (longs['a1'] << 32); //value = (longs['a0'] >>> 0) + ((longs['a1'] << 32) >>>0); offset += 8; break; case cc.DIF_FLOAT32: //not allowed according to wmbus standard, Qundis seems to use it nevertheless value = this.unpack('f', payload.substr(offset, 4)); offset += 4; break; case cc.DIF_VARLEN: var lvar = this.unpack('C', payload.substr(offset++, 1)) || 0; app.log.debug("in datablock " + dataBlockNo + ": LVAR field " + lvar.toString(16)); app.log.debug("payload len " + payload.length + " offset " + offset); if (lvar <= 0xbf) { if (dataBlock.type === "MANUFACTURER SPECIFIC") { // special handling, LSE seems to lie about this value = this.unpack('H*', payload.substr(offset, lvar)); app.log.debug("VALUE: " + value); } else { // ASCII string with LVAR characters value = this.unpack('a*', payload.substr(offset, lvar)); if (this.manufacturer === 'ESY') { // Easymeter stores the string backwards! value = value.split('').reverse().join(''); } } offset += lvar; } else if (lvar >= 0xc0 && lvar <= 0xcf) { // positive BCD number with (LVAR - C0h) • 2 digits value = decodeBCD((lvar - 0xc0) * 2, payload.substr(offset, (lvar - 0xc0))); offset += (lvar - 0xc0); } else if (lvar >= 0xd0 && lvar <= 0xdf) { // negative BCD number with (LVAR - D0h) • 2 digits value = -decodeBCD((lvar - 0xd0) * 2, payload.substr(offset, (lvar - 0xd0))); offset += (lvar - 0xd0); } else { this.errormsg = "in datablock " + dataBlockNo + ": unhandled LVAR field " + lvar.toString(16); this.errorcode = cc.ERR_UNKNOWN_LVAR; return 0; } break; case cc.DIF_SPECIAL: // special functions app.log.debug("DIF_SPECIAL at" + offset); value = this.unpack("H*", payload.substr(offset)); break PAYLOAD; default: this.errormsg = "in datablock " + dataBlockNo + ": unhandled datafield " + dataBlock.dataField.toString(16); this.errorcode = cc.ERR_UNKNOWN_DATAFIELD; return 0; } if (dataBlock.calcFunc != undefined) { dataBlock.value = dataBlock.calcFunc(value, dataBlock); app.log.debug("Value raw " + value + " value calc " + dataBlock.value); } else if (value !== undefined) { dataBlock.value = value; } else { dataBlock.value = ""; } var VIFExtensions = dataBlock.VIFExtensions; for (var i = 0; i < VIFExtensions.length; i++) { var VIFExtension = VIFExtensions[i]; dataBlock.extension = VIFExtension.unit; if (VIFExtension.calcFunc != undefined) { app.log.debug("Extension value " + VIFExtension.value + ", valueFactor " + VIFExtension.valueFactor); dataBlock.extension += ", " + VIFExtension.calcFunc(VIFExtension.value, dataBlock); } else if (VIFExtension.value != undefined) { dataBlock.extension += ", " + VIFExtension.value.toString(16); } else { //$dataBlock->{extension} = ""; } } value = undefined; dataBlocks.push(dataBlock) } this.datablocks = dataBlocks; return 1; }; WMBUS.prototype.decodeApplicationLayer = function () { if (this.crcRemoved) { var applicationlayer = this.msg.substr(10); } else { var applicationlayer = this.removeCRC(this.msg.substr(cc.TL_BLOCK_SIZE + this.crc_size)); if (this.errorcode != cc.ERR_NO_ERROR) { // CRC check failed return 0; } } app.log.debug(this.unpack("H*", applicationlayer)); this.cifield = applicationlayer.charCodeAt(0); var offset = 1; switch (this.cifield) { case cc.CI_RESP_4: app.log.debug("short header"); this.unpack('Caccess_no/Cstatus/ncw/', applicationlayer.substr(offset)); offset += 4; break; case cc.CI_RESP_12: app.log.debug("Long header"); this.unpack('Vmeter_id/vmeter_man/Cmeter_vers/Cmeter_dev/Caccess_no/Cstatus/ncw/', applicationlayer.substr(offset)); this.meter_id = decNo(this.meter_id); this.meter_devtypestring = validDeviceTypes[this.meter_dev] || 'unknown'; this.meter_manufacturer = uc(manId2ascii(this.meter_man)); offset += 12; break; case cc.CI_RESP_0: // no header this.cw = 0; break; default: // unsupported this.cw = 0; this.decodeConfigword(); this.errormsg = 'Unsupported CI Field ' + this.cifield.toString(16) + ", remaining payload is " + this.unpack("H*", applicationlayer.substr(offset)); this.errorcode = cc.ERR_UNKNOWN_CIFIELD; return 0; } this.statusstring = state2stri