import base64ToArrayBuffer from 'base64-arraybuffer'; import { asciiToBinaryCodedDecimal, charCodesToAscii, decimalArrayAsBigEndianNumber, decimalArrayAsLittleEndianNumber, decimalToBitfield, obisCodeValueToNumeric, parseBCD, powerFactorDecoding, uint8ArrayToAsciiNumber, } from './binary'; import { NeedMoreBytesError, ParserError } from './errors'; import { ObisParser, ChecksumValidation } from './ObisParser'; import { Parser } from './Parser'; import { ParserResult } from './ParserResult'; import { AntiCreepTimeModel, ConfigurationNumberModel, CurrentPhaseAModel, CurrentPhaseBModel, CurrentPhaseCModel, DemandAutoResetPeriodModel, DemandIntervalModel, DemandResetCounterModel, ErrorFlagsModel, FirmwareVersionModel, LastIntervalDemandExportModel, LastIntervalDemandImportModel, LastIntervalReactiveDemandExportModel, LastIntervalReactiveDemandImportModel, ManufacturingSerialNumberModel, MeterModelModel, NoLoadTimeModel, PhaseAReverseCountModel, PhaseFailCounterModel, PowerFactorModel, PowerFailCounterModel, PowerOnTimeModel, PresentIntervalDemandExportModel, PresentIntervalDemandImportModel, PresentIntervalReactiveDemandExportModel, PresentIntervalReactiveDemandImportModel, RegisterModel, RegisterResetCounterModel, SerialNumberModel, StatusFlags1Model, StatusFlags2Model, Tariff1ActiveEnergyAbsoluteModel, Tariff1CapacitiveReactiveEnergyAbsoluteModel, Tariff1InductiveReactiveEnergyAbsoluteModel, Tariff2ActiveEnergyAbsoluteModel, Tariff2CapacitiveReactiveEnergyAbsoluteModel, Tariff2InductiveReactiveEnergyAbsoluteModel, TimeToNextDemandAutoResetModel, TotalActiveEnergyExportModel, TotalActiveEnergyExportPreviousModel, TotalActiveEnergyImportModel, TotalActiveEnergyImportPreviousModel, TotalCapacitiveReactiveEnergyAbsoluteModel, TotalCumulativeMaximumDemandExportModel, TotalCumulativeMaximumDemandExportPreviousModel, TotalCumulativeMaximumDemandImportModel, TotalCumulativeMaximumDemandImportPreviousModel, TotalCumulativeMaximumReactiveDemandExportModel, TotalCumulativeMaximumReactiveDemandExportPreviousModel, TotalCumulativeMaximumReactiveDemandImportModel, TotalCumulativeMaximumReactiveDemandImportPreviousModel, TotalInductiveReactiveEnergyAbsoluteModel, TotalMaximumDemandExportModel, TotalMaximumDemandExportPreviousModel, TotalMaximumDemandImportModel, TotalMaximumDemandImportPreviousModel, TotalMaximumReactiveDemandExportModel, TotalMaximumReactiveDemandExportPreviousModel, TotalMaximumReactiveDemandImportModel, TotalMaximumReactiveDemandImportPreviousModel, TotalReactiveEnergyExportModel, TotalReactiveEnergyExportPreviousModel, TotalReactiveEnergyImportModel, TotalReactiveEnergyImportPreviousModel, TotalServiceTimeModel, VoltagePhaseAModel, VoltagePhaseBModel, VoltagePhaseCModel, WatchdogCounterModel, } from './registers'; const VARIABLE_BODY_LENGTH = 65535; const ELSTER_A150_V1_BODY_LENGTH = 73; const ELSTER_A150_V2_BODY_LENGTH = 87; export class ElsterParser extends Parser { parserForType = { 2: this.parseElsterA150.bind(this), 1: this.parseElsterA102.bind(this), 3: this.parseElsterA1052.bind(this), }; parseElsterA150V2(arrayBuffer: Uint8Array): ParserResult { const result = new ParserResult(); result.add(MeterModelModel, charCodesToAscii(arrayBuffer.slice(0, 12))); result.add(PowerFailCounterModel, parseBCD(arrayBuffer.slice(21, 23))); result.add(ConfigurationNumberModel, charCodesToAscii(arrayBuffer.slice(23, 27))); result.add(TotalActiveEnergyImportModel, parseBCD(arrayBuffer.slice(27, 32), 3)); result.add(TotalInductiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(32, 37), 3)); result.add(StatusFlags1Model, decimalToBitfield(arrayBuffer[37])); result.add(TotalServiceTimeModel, parseBCD(arrayBuffer.slice(38, 42))); result.add(TotalCapacitiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(47, 52), 3)); result.add(PowerFactorModel, parseBCD(arrayBuffer.slice(53, 55), 3)); result.add(CurrentPhaseAModel, parseBCD(arrayBuffer.slice(57, 59), 1)); result.add(VoltagePhaseAModel, parseBCD(arrayBuffer.slice(61, 63), 1)); result.add(AntiCreepTimeModel, parseBCD(arrayBuffer.slice(63, 66))); result.add(DemandResetCounterModel, (arrayBuffer[66] as number).toString()); result.add(PhaseAReverseCountModel, (arrayBuffer[67] as number).toString()); result.add(RegisterResetCounterModel, (arrayBuffer[68] as number).toString()); result.add(SerialNumberModel, uint8ArrayToAsciiNumber(arrayBuffer.slice(69, 78))); result.add(TotalMaximumDemandImportPreviousModel, parseBCD(arrayBuffer.slice(78, 81), 3)); result.add(TotalCumulativeMaximumDemandImportModel, parseBCD(arrayBuffer.slice(81, 84), 1)); result.add(TotalMaximumDemandImportModel, parseBCD(arrayBuffer.slice(84, 87), 3)); return result; } parseElsterA150V1(arrayBuffer: Uint8Array): ParserResult { const result = new ParserResult(); result.add(MeterModelModel, charCodesToAscii(arrayBuffer.slice(0, 12))); result.add(PowerFailCounterModel, parseBCD(arrayBuffer.slice(21, 23))); result.add(ConfigurationNumberModel, charCodesToAscii(arrayBuffer.slice(23, 27))); result.add(TotalActiveEnergyImportModel, parseBCD(arrayBuffer.slice(27, 32), 3)); result.add(TotalInductiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(32, 37), 3)); result.add(StatusFlags1Model, decimalToBitfield(arrayBuffer[37])); result.add(TotalServiceTimeModel, parseBCD(arrayBuffer.slice(38, 42))); result.add(TotalCapacitiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(42, 47), 3)); result.add(PowerFactorModel, parseBCD(arrayBuffer.slice(48, 50), 3)); result.add(CurrentPhaseAModel, parseBCD(arrayBuffer.slice(52, 54), 1)); result.add(VoltagePhaseAModel, parseBCD(arrayBuffer.slice(56, 58), 1)); result.add(AntiCreepTimeModel, parseBCD(arrayBuffer.slice(58, 61))); result.add(DemandResetCounterModel, (arrayBuffer[61] as number).toString()); result.add(PhaseAReverseCountModel, (arrayBuffer[62] as number).toString()); result.add(RegisterResetCounterModel, (arrayBuffer[63] as number).toString()); result.add(SerialNumberModel, uint8ArrayToAsciiNumber(arrayBuffer.slice(54, 73))); result.add(TotalMaximumDemandImportPreviousModel, null); result.add(TotalCumulativeMaximumDemandImportModel, null); result.add(TotalMaximumDemandImportModel, null); return result; } parseElsterA150(arrayBuffer: Uint8Array): ParserResult { if (arrayBuffer.length === ELSTER_A150_V1_BODY_LENGTH) { return this.parseElsterA150V1(arrayBuffer); } else if (arrayBuffer.length === ELSTER_A150_V2_BODY_LENGTH) { return this.parseElsterA150V2(arrayBuffer); } throw new ParserError('Unexpected A150 Body Length'); } parseElsterA102(arrayBuffer: Uint8Array): ParserResult { const result = new ParserResult(); result.add(MeterModelModel, charCodesToAscii(arrayBuffer.slice(0, 12))); result.add(FirmwareVersionModel, charCodesToAscii(arrayBuffer.slice(12, 21))); result.add(ManufacturingSerialNumberModel, decimalArrayAsBigEndianNumber(arrayBuffer.slice(21, 24))); result.add(ConfigurationNumberModel, decimalArrayAsBigEndianNumber(arrayBuffer.slice(24, 26))); result.add(SerialNumberModel, uint8ArrayToAsciiNumber(arrayBuffer.slice(26, 42))); const reg1Source = decimalToBitfield(arrayBuffer[42]); const reg2Source = decimalToBitfield(arrayBuffer[43]); const reg3Source = decimalToBitfield(arrayBuffer[44]); this.assert(reg1Source === '0b00000011', `reg1Source value is invalid: ${reg1Source}`); this.assert(reg2Source === '0b00001100', `reg2Source value is invalid: ${reg2Source}`); this.assert(reg3Source === '0b00110000', `reg3Source value is invalid: ${reg3Source}`); // Tariff 1 Registers result.add(Tariff1ActiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(45, 50), 3)); result.add(Tariff1InductiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(50, 55), 3)); result.add(Tariff1CapacitiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(55, 60), 3)); // Tariff 2 Registers result.add(Tariff2ActiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(60, 65), 3)); result.add(Tariff2InductiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(65, 70), 3)); result.add(Tariff2CapacitiveReactiveEnergyAbsoluteModel, parseBCD(arrayBuffer.slice(70, 75), 3)); const powerFactor = powerFactorDecoding(arrayBuffer[75] as number); result.add(PowerFactorModel, powerFactor); result.add(StatusFlags1Model, decimalToBitfield(arrayBuffer[76])); result.add(ErrorFlagsModel, decimalToBitfield(arrayBuffer[77])); result.add(AntiCreepTimeModel, parseBCD(arrayBuffer.slice(78, 81))); result.add(TotalServiceTimeModel, parseBCD(arrayBuffer.slice(81, 84))); const rateTime2 = parseInt(asciiToBinaryCodedDecimal(arrayBuffer.slice(84, 87)), 10); this.assert(rateTime2 === 0); result.add(PowerOnTimeModel, parseBCD(arrayBuffer.slice(87, 90))); result.add(PowerFailCounterModel, decimalArrayAsLittleEndianNumber(arrayBuffer.slice(90, 92))); result.add(WatchdogCounterModel, (arrayBuffer[92] as number).toString()); result.add(PhaseAReverseCountModel, (arrayBuffer[92] as number).toString()); return result; } elsterA1502CodeToObisMap: [RegisterModel, string][] = [ [TotalActiveEnergyImportModel, '1.8.0'], [TotalActiveEnergyExportModel, '2.8.0'], [TotalReactiveEnergyImportModel, '3.8.0'], [TotalReactiveEnergyExportModel, '4.8.0'], [SerialNumberModel, '0.0.0'], [MeterModelModel, '96.1.1'], [FirmwareVersionModel, '0.2.0'], [ConfigurationNumberModel, '0.2.1'], [StatusFlags1Model, '96.4.0'], [StatusFlags2Model, '96.5.0'], [TotalServiceTimeModel, '96.8.0'], [PowerFailCounterModel, '96.7.0'], [PhaseAReverseCountModel, '96.53.0'], [PhaseFailCounterModel, '96.54.0'], [NoLoadTimeModel, '96.55.0'], [PowerOnTimeModel, '96.56.0'], [CurrentPhaseAModel, '31.5.0'], [VoltagePhaseAModel, '32.5.0'], [CurrentPhaseBModel, '51.5.0'], [VoltagePhaseBModel, '52.5.0'], [CurrentPhaseCModel, '71.5.0'], [VoltagePhaseCModel, '72.5.0'], ]; elsterA1502WithDemandCodeToObisMap: [RegisterModel, string][] = [ [TotalActiveEnergyImportModel, '1.8.0'], [TotalActiveEnergyExportModel, '2.8.0'], [TotalReactiveEnergyImportModel, '3.8.0'], [TotalReactiveEnergyExportModel, '4.8.0'], [SerialNumberModel, '0.0.0'], [MeterModelModel, '96.1.1'], [FirmwareVersionModel, '0.2.0'], [ConfigurationNumberModel, '0.2.1'], [StatusFlags1Model, '96.4.0'], [StatusFlags2Model, '96.5.0'], [TotalServiceTimeModel, '96.8.0'], [PowerFailCounterModel, '96.7.0'], [PhaseAReverseCountModel, '96.53.0'], [PhaseFailCounterModel, '96.54.0'], [NoLoadTimeModel, '96.55.0'], [PowerOnTimeModel, '96.56.0'], [CurrentPhaseAModel, '31.5.0'], [VoltagePhaseAModel, '32.5.0'], [CurrentPhaseBModel, '51.5.0'], [VoltagePhaseBModel, '52.5.0'], [CurrentPhaseCModel, '71.5.0'], [VoltagePhaseCModel, '72.5.0'], [PresentIntervalDemandImportModel, '1.4.0'], [PresentIntervalDemandExportModel, '2.4.0'], [PresentIntervalReactiveDemandImportModel, '3.4.0'], [PresentIntervalReactiveDemandExportModel, '4.4.0'], [LastIntervalDemandImportModel, '1.5.0'], [LastIntervalDemandExportModel, '2.5.0'], [LastIntervalReactiveDemandImportModel, '3.5.0'], [LastIntervalReactiveDemandExportModel, '4.5.0'], [TotalMaximumDemandImportModel, '1.6.0'], [TotalMaximumDemandExportModel, '2.6.0'], [TotalMaximumReactiveDemandImportModel, '3.6.0'], [TotalMaximumReactiveDemandExportModel, '4.6.0'], [TotalCumulativeMaximumDemandImportModel, '1.2.0'], [TotalCumulativeMaximumDemandExportModel, '2.2.0'], [TotalCumulativeMaximumReactiveDemandImportModel, '3.2.0'], [TotalCumulativeMaximumReactiveDemandExportModel, '4.2.0'], [TotalActiveEnergyImportPreviousModel, '1.8.0.1'], [TotalActiveEnergyExportPreviousModel, '2.8.0.1'], [TotalReactiveEnergyImportPreviousModel, '3.8.0.1'], [TotalReactiveEnergyExportPreviousModel, '4.8.0.1'], [TotalMaximumDemandImportPreviousModel, '1.6.0.1'], [TotalMaximumDemandExportPreviousModel, '2.6.0.1'], [TotalMaximumReactiveDemandImportPreviousModel, '3.6.0.1'], [TotalMaximumReactiveDemandExportPreviousModel, '4.6.0.1'], [TotalCumulativeMaximumDemandImportPreviousModel, '1.2.0.1'], [TotalCumulativeMaximumDemandExportPreviousModel, '2.2.0.1'], [TotalCumulativeMaximumReactiveDemandImportPreviousModel, '3.2.0.1'], [TotalCumulativeMaximumReactiveDemandExportPreviousModel, '4.2.0.1'], [RegisterResetCounterModel, '96.57.0'], [DemandIntervalModel, '96.58.0'], [DemandResetCounterModel, '0.1.0'], [DemandAutoResetPeriodModel, '96.59.0'], [TimeToNextDemandAutoResetModel, '96.60.0'], ]; getCodeMapper() { switch (this.interpretationCode) { case 1: return this.elsterA1502CodeToObisMap; case 2: return this.elsterA1502WithDemandCodeToObisMap; default: throw new ParserError('Invalid interpretation code'); } } parseElsterA1052(arrayBuffer: Uint8Array): ParserResult { const arraySlice = arrayBuffer.slice(1); const obisParser = new ObisParser(); const obisRegisters = obisParser.parse(arraySlice, { checksumValidation: ChecksumValidation.CRC_INCLUDE_BOTH, }); const result = new ParserResult(); const addValue = (entry: [RegisterModel, string]) => { const [recordModel, targetObis] = entry; const rawValue = obisRegisters.getValueOfObis(targetObis); if (rawValue == null) { result.add(recordModel, null); return; } let finalValue: string | null = rawValue; if (recordModel.format === 'numeric') { finalValue = obisCodeValueToNumeric(rawValue); } if (recordModel.format === 'bitfield') { finalValue = decimalToBitfield(rawValue); } if (recordModel.format === 'text') { finalValue = rawValue.trim(); } result.add(recordModel, finalValue); }; for (const entry of this.getCodeMapper()) { addValue(entry); } return result; } parse(aBase64String: string): ParserResult { let arrayBuffer = new Uint8Array(base64ToArrayBuffer.decode(aBase64String)); if (arrayBuffer.length < 4) { throw new NeedMoreBytesError(); } const headerLength = Number(decimalArrayAsBigEndianNumber(arrayBuffer.slice(2, 4))); if (arrayBuffer.length < headerLength) { throw new NeedMoreBytesError(); } const bodyLength = Number(decimalArrayAsBigEndianNumber(arrayBuffer.slice(4, 6))); if (bodyLength !== VARIABLE_BODY_LENGTH && arrayBuffer.length < headerLength + bodyLength) { throw new NeedMoreBytesError(); } const headerArrayBuffer = arrayBuffer.slice(0, headerLength); const bodyArrayBuffer = arrayBuffer.slice(headerLength, headerLength + bodyLength); const typeByte = headerArrayBuffer[8] ?? -1; const finalParser = this.parserForType[typeByte]; this.assert(!!finalParser, `Unknown parser type: ${typeByte}`); return finalParser(bodyArrayBuffer, typeByte); } }