From b76b77b074db4cc435f839be7a9970404e39882a Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Wed, 21 May 2025 15:01:15 +0200 Subject: [PATCH 01/18] feat: add support for dlms/cosem --- .gitignore | 4 +- package.json | 2 +- src/index.ts | 121 +----- src/parsers/cosem.ts | 392 ------------------ src/parsers/dsmr.ts | 137 ------ src/parsers/stream.ts | 53 --- src/protocols/cosem.ts | 355 ++++++++++++++++ src/protocols/dlms-datatype.ts | 255 ++++++++++++ src/protocols/dlms-payload/BasicList.ts | 75 ++++ src/protocols/dlms-payload/BasicStructure.ts | 172 ++++++++ src/protocols/dlms-payload/DescribedList.ts | 70 ++++ src/protocols/dlms-payload/dlms-payload.ts | 171 ++++++++ src/protocols/dlms-payload/dlms-payloads.ts | 31 ++ src/protocols/dlms.ts | 107 +++++ src/protocols/dsmr.ts | 277 +++++++++++++ src/{util => protocols}/encryption.ts | 120 +++--- src/protocols/hdlc.ts | 203 +++++++++ src/protocols/obis-code.ts | 155 +++++++ src/stream/stream-detect-type.ts | 221 ++++++++++ src/stream/stream-dlms.ts | 204 +++++++++ .../stream-encrypted-dsmr.ts} | 74 ++-- .../stream-unencrypted-dsmr.ts} | 101 +---- src/stream/stream.ts | 16 + src/util/base-result.ts | 94 +++++ src/util/crc.ts | 96 +++-- src/util/errors.ts | 37 +- src/util/frame-validation.ts | 52 --- src/{parsers => util}/mbus.ts | 4 +- tests/crc.spec.ts | 61 --- tests/frame-validation.spec.ts | 71 ---- .../dsmr.spec.ts} | 73 ++-- tests/{ => protocols}/encryption.spec.ts | 48 +-- tests/protocols/obis-code.spec.ts | 230 ++++++++++ tests/stream/stream-detect-type.spec.ts | 239 +++++++++++ .../stream-dsmr.spec.ts} | 294 +++---------- tests/telegrams/dlms/aidon-example-1.json | 56 +++ tests/telegrams/dlms/aidon-example-1.txt | 13 + tests/telegrams/dlms/aidon-example-2.json | 90 ++++ tests/telegrams/dlms/aidon-example-2.txt | 31 ++ .../encrypted/aidon-example-2-with-aad.txt | 41 ++ .../encrypted/aidon-example-2-without-aad.txt | 41 ++ tests/telegrams/dlms/kamstrup-example-1.json | 61 +++ tests/telegrams/dlms/kamstrup-example-1.txt | 17 + tests/telegrams/dlms/kamstrup-example-2.json | 71 ++++ tests/telegrams/dlms/kamstrup-example-2.txt | 22 + tests/telegrams/dlms/kamstrup-example-3.json | 55 +++ tests/telegrams/dlms/kamstrup-example-3.txt | 12 + .../telegrams/{ => dsmr}/dsmr-2.2-kfm-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-2.2-kfm-1.txt | 0 .../{ => dsmr}/dsmr-3.0-spec-example.json | 0 .../{ => dsmr}/dsmr-3.0-spec-example.txt | 0 .../telegrams/{ => dsmr}/dsmr-4.0-isk-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-4.0-isk-1.txt | 0 .../telegrams/{ => dsmr}/dsmr-4.0-isk-2.json | 0 tests/telegrams/{ => dsmr}/dsmr-4.0-isk-2.txt | 0 .../{ => dsmr}/dsmr-4.0-spec-example.json | 0 .../{ => dsmr}/dsmr-4.0-spec-example.txt | 0 .../telegrams/{ => dsmr}/dsmr-4.2-kfm-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-4.2-kfm-1.txt | 0 .../telegrams/{ => dsmr}/dsmr-4.2-xmx-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-4.2-xmx-1.txt | 0 .../{ => dsmr}/dsmr-4.2.2-spec-example.json | 0 .../{ => dsmr}/dsmr-4.2.2-spec-example.txt | 0 .../telegrams/{ => dsmr}/dsmr-5.0-ene-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-5.0-ene-1.txt | 0 .../telegrams/{ => dsmr}/dsmr-5.0-ene-2.json | 0 tests/telegrams/{ => dsmr}/dsmr-5.0-ene-2.txt | 0 .../telegrams/{ => dsmr}/dsmr-5.0-ene-3.json | 0 tests/telegrams/{ => dsmr}/dsmr-5.0-ene-3.txt | 0 .../{ => dsmr}/dsmr-5.0-est-units.json | 0 .../{ => dsmr}/dsmr-5.0-est-units.txt | 0 .../telegrams/{ => dsmr}/dsmr-5.0-isk-1.json | 0 tests/telegrams/{ => dsmr}/dsmr-5.0-isk-1.txt | 0 .../dsmr-5.0-spec-example-lowercase.json | 0 .../dsmr-5.0-spec-example-lowercase.txt | 0 .../{ => dsmr}/dsmr-5.0-spec-example.json | 0 .../{ => dsmr}/dsmr-5.0-spec-example.txt | 0 .../dsmr-luxembourgh-spec-example.json | 0 .../dsmr-luxembourgh-spec-example.txt | 0 .../emucs-p1-v2.1.1-spec-example-1.json | 0 .../emucs-p1-v2.1.1-spec-example-1.txt | 0 .../emucs-p1-v2.1.1-spec-example-2.json | 0 .../emucs-p1-v2.1.1-spec-example-2.txt | 0 ...dsmr-luxembourgh-spec-example-with-aad.txt | 0 ...r-luxembourgh-spec-example-without-aad.txt | 0 ...iskra-mt-382-no-crc-with-text-message.json | 0 .../iskra-mt-382-no-crc-with-text-message.txt | 0 .../{ => dsmr}/iskra-mt-382-no-crc.json | 0 .../{ => dsmr}/iskra-mt-382-no-crc.txt | 0 .../kamstrup-OMNIA-e-meter-three-phase.json | 0 .../kamstrup-OMNIA-e-meter-three-phase.txt | 0 .../telegrams/{ => dsmr}/sagemcom-xt211.json | 0 tests/telegrams/{ => dsmr}/sagemcom-xt211.txt | 0 tests/telegrams/{ => dsmr}/unknown-xmx-1.json | 0 tests/telegrams/{ => dsmr}/unknown-xmx-1.txt | 0 tests/telegrams/m-bus/austria-decrypted-1.txt | 1 + .../m-bus/austria-example-1 copy.txt | 8 + tests/telegrams/m-bus/austria-example-1.txt | 19 + tests/test-utils.ts | 110 ++++- tests/util/crc.spec.ts | 73 ++++ tools/decrypt-telegram.ts | 8 +- tools/parse-dlms.ts | 89 ++++ tools/parse-hdlc.ts | 115 +++++ tools/parse-telegram.ts | 127 ++++-- tools/update-test-telegrams.ts | 160 +++++-- tools/wrap-hdlc.ts | 27 ++ tsconfig.json | 2 +- 107 files changed, 4361 insertions(+), 1503 deletions(-) delete mode 100644 src/parsers/cosem.ts delete mode 100644 src/parsers/dsmr.ts delete mode 100644 src/parsers/stream.ts create mode 100644 src/protocols/cosem.ts create mode 100644 src/protocols/dlms-datatype.ts create mode 100644 src/protocols/dlms-payload/BasicList.ts create mode 100644 src/protocols/dlms-payload/BasicStructure.ts create mode 100644 src/protocols/dlms-payload/DescribedList.ts create mode 100644 src/protocols/dlms-payload/dlms-payload.ts create mode 100644 src/protocols/dlms-payload/dlms-payloads.ts create mode 100644 src/protocols/dlms.ts create mode 100644 src/protocols/dsmr.ts rename src/{util => protocols}/encryption.ts (51%) create mode 100644 src/protocols/hdlc.ts create mode 100644 src/protocols/obis-code.ts create mode 100644 src/stream/stream-detect-type.ts create mode 100644 src/stream/stream-dlms.ts rename src/{parsers/stream-encrypted.ts => stream/stream-encrypted-dsmr.ts} (66%) rename src/{parsers/stream-unencrypted.ts => stream/stream-unencrypted-dsmr.ts} (57%) create mode 100644 src/stream/stream.ts create mode 100644 src/util/base-result.ts delete mode 100644 src/util/frame-validation.ts rename src/{parsers => util}/mbus.ts (82%) delete mode 100644 tests/crc.spec.ts delete mode 100644 tests/frame-validation.spec.ts rename tests/{dsmr-parser.spec.ts => protocols/dsmr.spec.ts} (60%) rename tests/{ => protocols}/encryption.spec.ts (59%) create mode 100644 tests/protocols/obis-code.spec.ts create mode 100644 tests/stream/stream-detect-type.spec.ts rename tests/{stream.spec.ts => stream/stream-dsmr.spec.ts} (61%) create mode 100644 tests/telegrams/dlms/aidon-example-1.json create mode 100644 tests/telegrams/dlms/aidon-example-1.txt create mode 100644 tests/telegrams/dlms/aidon-example-2.json create mode 100644 tests/telegrams/dlms/aidon-example-2.txt create mode 100644 tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt create mode 100644 tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt create mode 100644 tests/telegrams/dlms/kamstrup-example-1.json create mode 100644 tests/telegrams/dlms/kamstrup-example-1.txt create mode 100644 tests/telegrams/dlms/kamstrup-example-2.json create mode 100644 tests/telegrams/dlms/kamstrup-example-2.txt create mode 100644 tests/telegrams/dlms/kamstrup-example-3.json create mode 100644 tests/telegrams/dlms/kamstrup-example-3.txt rename tests/telegrams/{ => dsmr}/dsmr-2.2-kfm-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-2.2-kfm-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-3.0-spec-example.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-3.0-spec-example.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-isk-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-isk-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-isk-2.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-isk-2.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-spec-example.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.0-spec-example.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2-kfm-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2-kfm-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2-xmx-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2-xmx-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2.2-spec-example.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-4.2.2-spec-example.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-2.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-2.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-3.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-ene-3.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-est-units.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-est-units.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-isk-1.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-isk-1.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-spec-example-lowercase.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-spec-example-lowercase.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-spec-example.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-5.0-spec-example.txt (100%) rename tests/telegrams/{ => dsmr}/dsmr-luxembourgh-spec-example.json (100%) rename tests/telegrams/{ => dsmr}/dsmr-luxembourgh-spec-example.txt (100%) rename tests/telegrams/{ => dsmr}/emucs-p1-v2.1.1-spec-example-1.json (100%) rename tests/telegrams/{ => dsmr}/emucs-p1-v2.1.1-spec-example-1.txt (100%) rename tests/telegrams/{ => dsmr}/emucs-p1-v2.1.1-spec-example-2.json (100%) rename tests/telegrams/{ => dsmr}/emucs-p1-v2.1.1-spec-example-2.txt (100%) rename tests/telegrams/{ => dsmr}/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt (100%) rename tests/telegrams/{ => dsmr}/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt (100%) rename tests/telegrams/{ => dsmr}/iskra-mt-382-no-crc-with-text-message.json (100%) rename tests/telegrams/{ => dsmr}/iskra-mt-382-no-crc-with-text-message.txt (100%) rename tests/telegrams/{ => dsmr}/iskra-mt-382-no-crc.json (100%) rename tests/telegrams/{ => dsmr}/iskra-mt-382-no-crc.txt (100%) rename tests/telegrams/{ => dsmr}/kamstrup-OMNIA-e-meter-three-phase.json (100%) rename tests/telegrams/{ => dsmr}/kamstrup-OMNIA-e-meter-three-phase.txt (100%) rename tests/telegrams/{ => dsmr}/sagemcom-xt211.json (100%) rename tests/telegrams/{ => dsmr}/sagemcom-xt211.txt (100%) rename tests/telegrams/{ => dsmr}/unknown-xmx-1.json (100%) rename tests/telegrams/{ => dsmr}/unknown-xmx-1.txt (100%) create mode 100644 tests/telegrams/m-bus/austria-decrypted-1.txt create mode 100644 tests/telegrams/m-bus/austria-example-1 copy.txt create mode 100644 tests/telegrams/m-bus/austria-example-1.txt create mode 100644 tests/util/crc.spec.ts create mode 100644 tools/parse-dlms.ts create mode 100644 tools/parse-hdlc.ts create mode 100644 tools/wrap-hdlc.ts diff --git a/.gitignore b/.gitignore index 9e043d6..b9bdb1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules /dist -/private \ No newline at end of file +/private +/tests/telegrams/dlms/*-data.json +/tests/telegrams/dlms/*-data.txt \ No newline at end of file diff --git a/package.json b/package.json index 2f5434d..d14416f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ".": "./dist/index.js" }, "scripts": { - "test": "node --import tsx --test-reporter spec --test ./tests/*.spec.ts", + "test": "node --import tsx --test-reporter spec --test ./tests/**/*.spec.ts", "lint": "eslint . && npm run prettier:check", "build": "tsc", "prettier": "prettier . --write", diff --git a/src/index.ts b/src/index.ts index d6bf3bc..cd9f7e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,119 +1,18 @@ -import { DSMRParser } from './parsers/dsmr.js'; -import { getMbusDevice, MBUS_DEVICE_IDS } from './parsers/mbus.js'; -import { createDSMRStreamParser, createDSMRStreamTransformer } from './parsers/stream.js'; -import { ENCRYPTION_DEFAULT_AAD } from './util/encryption.js'; -import { DSMRFrameValid } from './util/frame-validation.js'; +import { getMbusDevice, MBUS_DEVICE_IDS } from './util/mbus.js'; +import { ENCRYPTED_DLMS_DEFAULT_AAD } from './protocols/encryption.js'; +import { DsmrParserResult } from './protocols/dsmr.js'; +import { HdlcParserResult } from './protocols/hdlc.js'; -export type DSMRParserOptions = - | { - /** Raw DSMR telegram */ - telegram: string; - /** New line characters */ - newLineChars?: '\r\n' | '\n'; - /** Enable the encryption detection mechanism. Enabled by default */ - decryptionKey?: never; - additionalAuthenticatedData?: never; - encoding?: never; - } - | { - /** Encrypted DSMR telegram */ - telegram: Buffer; - /** Decryption key */ - decryptionKey?: Buffer; - /** AAD */ - additionalAuthenticatedData?: Buffer; - /** Encoding of the data in the buffer, defaults to binary */ - encoding?: BufferEncoding; - /** New line characters */ - newLineChars?: '\r\n' | '\n'; - }; - -export type DSMRParserResult = { - raw: string; - header: { - identifier: string; - xxx: string; - z: string; - }; - metadata: { - dsmrVersion?: number; - timestamp?: string; // TODO make this a date object - equipmentId?: string; - events?: { - powerFailures?: number; - longPowerFailures?: number; - voltageSags?: { - l1?: number; - l2?: number; - l3?: number; - }; - voltageSwells?: { - l1?: number; - l2?: number; - l3?: number; - }; - }; - unknownLines?: string[]; - textMessage?: string; - numericMessage?: number; - }; - electricity: { - total?: { - received?: number; - returned?: number; - }; - tariffs?: Partial>; - currentTariff?: number; - voltage?: { - l1?: number; - l2?: number; - l3?: number; - }; - current?: { - l1?: number; - l2?: number; - l3?: number; - }; - powerReturnedTotal?: number; - powerReturned?: { - l1?: number; - l2?: number; - l3?: number; - }; - powerReceivedTotal?: number; - powerReceived?: { - l1?: number; - l2?: number; - l3?: number; - }; - }; - mBus: Record< - number, - { - deviceType?: number; // TODO: Parse to device type? - equipmentId?: string; - value?: number; - unit?: string; - timestamp?: string; // TODO: Parse to date object - recordingPeriodMinutes?: number; // DSMR - } - >; - crc?: { - value: number; - valid: boolean; - }; - /** Only set when encryption is used */ - additionalAuthenticatedDataValid?: boolean; -}; +export type SmartMeterParserResult = DsmrParserResult | HdlcParserResult; export * from './util/errors.js'; export const DSMR = { - parse: DSMRParser, - createStreamParser: createDSMRStreamParser, - createStreamTransformer: createDSMRStreamTransformer, - isValidFrame: DSMRFrameValid, MBUS_DEVICE_IDS, getMbusDevice, - ENCRYPTION_DEFAULT_AAD, + ENCRYPTION_DEFAULT_AAD: ENCRYPTED_DLMS_DEFAULT_AAD, } as const; + +export { EncryptedDSMRStreamParser } from './stream/stream-encrypted-dsmr.js'; +export { UnencryptedDSMRStreamParser } from './stream/stream-unencrypted-dsmr.js'; +export { DlmsStreamParser } from './stream/stream-dlms.js'; diff --git a/src/parsers/cosem.ts b/src/parsers/cosem.ts deleted file mode 100644 index 03053ff..0000000 --- a/src/parsers/cosem.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { DSMRParserOptions, DSMRParserResult } from '../index.js'; - -export type COSEMDecoder = { - regex: RegExp; - parser: (opts: { - regexResult: RegExpExecArray; - result: DSMRParserResult; - options: DSMRParserOptions; - line: string; - lines: string[]; - lineNumber: number; - }) => void; -}; - -export const COSEM_PARSERS: COSEMDecoder[] = [ - { - regex: /^1-3:0\.2\.8\((\d+)\)/, - parser: ({ regexResult, result }) => { - const parsed = parseInt(regexResult[1], 10) / 10; - result.metadata.dsmrVersion = parsed; - }, - }, - { - regex: /^0-0:1\.0\.0\((\w+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.timestamp = regexResult[1]; // TODO: Parse to date object - }, - }, - { - regex: /^0-0:96\.1\.1\((\w+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.equipmentId = regexResult[1] ?? ''; - }, - }, - { - regex: /^1-(\d):1\.8\.(\d+)\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - const tariff = parseInt(regexResult[2], 10); - let value = parseFloat(regexResult[3]); - - const unit = regexResult[5]; - - if (unit === 'Wh') { - // Convert to kWh - value = value / 1000; - } - - if (tariff === 0) { - // This is the total received electricity - result.electricity.total = result.electricity.total ?? {}; - result.electricity.total.received = value; - } else { - // This is a specific tariff - result.electricity.tariffs = result.electricity.tariffs ?? {}; - result.electricity.tariffs[tariff] = result.electricity.tariffs[tariff] ?? {}; - result.electricity.tariffs[tariff].received = value; - } - }, - }, - { - regex: /^1-(\d):2\.8\.(\d+)\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - const tariff = parseInt(regexResult[2], 10); - - let value = parseFloat(regexResult[3]); - - const unit = regexResult[5]; - - if (unit === 'Wh') { - // Convert to kWh - value = value / 1000; - } - - if (tariff === 0) { - // This is the total received electricity - result.electricity.total = result.electricity.total ?? {}; - result.electricity.total.returned = value; - } else { - // This is a specific tariff - result.electricity.tariffs = result.electricity.tariffs ?? {}; - result.electricity.tariffs[tariff] = result.electricity.tariffs[tariff] ?? {}; - result.electricity.tariffs[tariff].returned = value; - } - }, - }, - { - regex: /^0-0:96\.14\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.electricity.currentTariff = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-(\d):1\.7\.0\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - let value = parseFloat(regexResult[2]); - const unit = regexResult[4]; - - if (unit === 'W') { - // Convert to kW - value = value / 1000; - } - - result.electricity.powerReceivedTotal = value; - }, - }, - { - regex: /^1-(\d):2\.7\.0\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - let value = parseFloat(regexResult[2]); - const unit = regexResult[4]; - - if (unit === 'W') { - // Convert to kW - value = value / 1000; - } - - result.electricity.powerReturnedTotal = value; - }, - }, - { - regex: /^0-0:96\.7\.21\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.powerFailures = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^0-0:96\.7\.9\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.longPowerFailures = parseInt(regexResult[1], 10); - }, - }, - // 1-0:99.97.0 - { - regex: /^1-0:32\.32\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; - result.metadata.events.voltageSags.l1 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-0:52\.32\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; - result.metadata.events.voltageSags.l2 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-0:72\.32\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; - result.metadata.events.voltageSags.l3 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-0:32\.36\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; - result.metadata.events.voltageSwells.l1 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-0:52\.36\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; - result.metadata.events.voltageSwells.l2 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^1-0:72\.36\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - result.metadata.events = result.metadata.events ?? {}; - result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; - result.metadata.events.voltageSwells.l3 = parseInt(regexResult[1], 10); - }, - }, - { - regex: /^0-0:96\.13\.0\((.+)?\)/, - parser: ({ regexResult, result }) => { - result.metadata.textMessage = regexResult[1] ?? ''; - }, - }, - { - regex: /^0-0:96\.13\.1\((\d)?\)/, - parser: ({ regexResult, result }) => { - const numericMessage = parseInt(regexResult[1], 10); - result.metadata.numericMessage = Number.isNaN(numericMessage) ? 0 : numericMessage; - }, - }, - { - regex: /^1-0:32\.7\.0\((\d+(\.\d+)?)\*(V|v)\)/, - parser: ({ regexResult, result }) => { - result.electricity.voltage = result.electricity.voltage ?? {}; - result.electricity.voltage.l1 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:52\.7\.0\((\d+(\.\d+)?)\*(V|v)\)/, - parser: ({ regexResult, result }) => { - result.electricity.voltage = result.electricity.voltage ?? {}; - result.electricity.voltage.l2 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:72\.7\.0\((\d+(\.\d+)?)\*(V|v)\)/, - parser: ({ regexResult, result }) => { - result.electricity.voltage = result.electricity.voltage ?? {}; - result.electricity.voltage.l3 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:31\.7\.0\((\d+(\.\d+)?)\*(A|a)\)/, - parser: ({ regexResult, result }) => { - result.electricity.current = result.electricity.current ?? {}; - result.electricity.current.l1 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:51\.7\.0\((\d+(\.\d+)?)\*(A|a)\)/, - parser: ({ regexResult, result }) => { - result.electricity.current = result.electricity.current ?? {}; - result.electricity.current.l2 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:71\.7\.0\((\d+(\.\d+)?)\*(A|a)\)/, - parser: ({ regexResult, result }) => { - result.electricity.current = result.electricity.current ?? {}; - result.electricity.current.l3 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:21\.7\.0\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - let value = parseFloat(regexResult[1]); - const unit = regexResult[3]; - - if (unit === 'W') { - // Convert to kW - value = value / 1000; - } - - result.electricity.powerReceived = result.electricity.powerReceived ?? {}; - result.electricity.powerReceived.l1 = value; - }, - }, - { - regex: /^1-0:41\.7\.0\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - let value = parseFloat(regexResult[1]); - const unit = regexResult[3]; - - if (unit === 'W') { - // Convert to kW - value = value / 1000; - } - - result.electricity.powerReceived = result.electricity.powerReceived ?? {}; - result.electricity.powerReceived.l2 = value; - }, - }, - { - regex: /^1-0:61\.7\.0\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - let value = parseFloat(regexResult[1]); - const unit = regexResult[3]; - - if (unit === 'W') { - // Convert to kW - value = value / 1000; - } - - result.electricity.powerReceived = result.electricity.powerReceived ?? {}; - result.electricity.powerReceived.l3 = value; - }, - }, - { - regex: /^1-0:22\.7\.0\((\d+(\.\d+)?)\*(kW|kw)\)/, - parser: ({ regexResult, result }) => { - result.electricity.powerReturned = result.electricity.powerReturned ?? {}; - result.electricity.powerReturned.l1 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:42\.7\.0\((\d+(\.\d+)?)\*(kW|kw)\)/, - parser: ({ regexResult, result }) => { - result.electricity.powerReturned = result.electricity.powerReturned ?? {}; - result.electricity.powerReturned.l2 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^1-0:62\.7\.0\((\d+(\.\d+)?)\*(kW|kw)\)/, - parser: ({ regexResult, result }) => { - result.electricity.powerReturned = result.electricity.powerReturned ?? {}; - result.electricity.powerReturned.l3 = parseFloat(regexResult[1]); - }, - }, - { - regex: /^0-(\d):24\.1\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - const busId = parseInt(regexResult[1], 10); - const typeId = parseInt(regexResult[2], 10); - result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].deviceType = typeId; - }, - }, - { - regex: /^0-(\d):96\.1\.0\((\d+)\)/, - parser: ({ regexResult, result }) => { - const busId = parseInt(regexResult[1], 10); - const equipmentId = regexResult[2]; - result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].equipmentId = equipmentId; - }, - }, - // { - // regex: /^0-\d:24\.2\.1\((\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d+)\*m3\)/, - // parser: ({ regexResult, result }) => { - // const year = `20${regexResult[1]}`; - // const dateTime = new Date(`${year}-${regexResult[2]}-${regexResult[3]}T${regexResult[4]}:${regexResult[5]}:${regexResult[6]}Z`); - // result.mBus.valueTimestamp = dateTime.toISOString(); - // result.mBus.value = parseFloat(regexResult[7]); - // } - // }, - { - regex: /^0-(\d):24\.2\.1\((\w+)\)\((\d+(\.\d+)?)\*?(\w+)?\)/, - parser: ({ regexResult, result }) => { - const busId = parseInt(regexResult[1], 10); - const timestamp = regexResult[2]; - const value = parseFloat(regexResult[3]); - const unit = regexResult[5]; - result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].timestamp = timestamp; - result.mBus[busId].value = value; - result.mBus[busId].unit = unit; - }, - }, - // This is the gas/water data for Belgium/eMUCS meters - { - regex: /^0-(\d):24\.2\.3\((\w+)\)\((\d+(\.\d+)?)\*(\w+)\)/, - parser: ({ regexResult, result }) => { - const busId = parseInt(regexResult[1], 10); - const timestamp = regexResult[2]; - const value = parseFloat(regexResult[3]); - const unit = regexResult[5]; - result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].timestamp = timestamp; - result.mBus[busId].value = value; - result.mBus[busId].unit = unit; - }, - }, - // Gas report for DSMR 3 meters. - // This is a bit off an odd one, as it's a two line parser. - // 0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3) - // (00000.000) - { - regex: /^0-(\d):24\.3\.0\((\w+)\)\((\d+)\)\((\d+)\)\((\d)\)\((0-\d:24\.2\.1)\)\((\w+)\)/, - parser({ regexResult, result, lines, lineNumber }) { - const busId = parseInt(regexResult[1], 10); - const timestamp = regexResult[2]; - const recordingPeriodMinutes = parseInt(regexResult[4], 10); - const unit = regexResult[7]; - const nextLine = lines[lineNumber + 1]; - - if (!nextLine) { - return; - } - - const valueRegex = /\((\d+(\.\d+)?)\)/; - const valueMatch = valueRegex.exec(nextLine); - - if (!valueMatch) { - return; - } - - const value = parseFloat(valueMatch[1]); - - result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].timestamp = timestamp; - result.mBus[busId].value = value; - result.mBus[busId].unit = unit; - result.mBus[busId].value = value; - result.mBus[busId].recordingPeriodMinutes = recordingPeriodMinutes; - }, - }, -] as const; diff --git a/src/parsers/dsmr.ts b/src/parsers/dsmr.ts deleted file mode 100644 index 6b4aa6a..0000000 --- a/src/parsers/dsmr.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { DSMRParserOptions, DSMRParserResult } from '../index.js'; -import { isCrcValid } from '../util/crc.js'; -import { decryptFrame } from '../util/encryption.js'; -import { DSMRParserError } from '../util/errors.js'; -import { DEFAULT_FRAME_ENCODING } from '../util/frame-validation.js'; -import { COSEM_PARSERS } from './cosem.js'; - -const decodeCOSEMObject = ({ - line, - lines, - lineNumber, - result, - options, -}: { - line: string; - lines: string[]; - lineNumber: number; - result: DSMRParserResult; - options: DSMRParserOptions; -}) => { - for (const { regex, parser } of COSEM_PARSERS) { - const regexResult = regex.exec(line); - - if (regexResult) { - parser({ - regexResult, - result, - options, - line, - lines, - lineNumber, - }); - return true; - } - } - - return false; -}; - -/** - * Parse a DSMR telegram into a structured object. - * - * @throws If CRC validation fails - */ -export const DSMRParser = (options: DSMRParserOptions): DSMRParserResult => { - options.newLineChars = options.newLineChars ?? '\r\n'; - - let telegram: string; - let decryptError: Error | undefined; - - if (typeof options.telegram === 'string') { - telegram = options.telegram; - } else if (!Buffer.isBuffer(options.decryptionKey)) { - telegram = options.telegram.toString(options.encoding ?? DEFAULT_FRAME_ENCODING); - } else { - const { content, error } = decryptFrame({ - data: options.telegram, - key: options.decryptionKey, - additionalAuthenticatedData: options.additionalAuthenticatedData, - encoding: options.encoding ?? DEFAULT_FRAME_ENCODING, - }); - - telegram = content; - decryptError = error; - } - - const lines = telegram.split(options.newLineChars); - - const result: DSMRParserResult = { - raw: telegram, - header: { - identifier: '', - xxx: '', - z: '', - }, - metadata: {}, - electricity: {}, - mBus: {}, - }; - - let objectsParsed = 0; - - for (const [lineNumber, line] of lines.entries()) { - if (line.startsWith('/')) { - // Beginning of telegram - result.header.xxx = line.slice(1, 4); - result.header.z = line.slice(4, 5); - result.header.identifier = line.slice(5, line.length); - } else if (line.startsWith('!')) { - // End of telegram - if (line.length > 1) { - result.crc = { - value: parseInt(line.slice(1, line.length), 16), - valid: false, - }; - } - } else if (line === '' || line === '\0') { - // skip empty lines - } else { - // Decode cosem object - const isLineParsed = decodeCOSEMObject({ - result, - options, - line, - lines, - lineNumber, - }); - - if (!isLineParsed) { - result.metadata.unknownLines = result.metadata.unknownLines ?? []; - result.metadata.unknownLines.push(line); - } else { - objectsParsed++; - } - } - } - - if (result.crc !== undefined) { - result.crc.valid = isCrcValid({ - telegram, - crc: result.crc.value, - newLineChars: options.newLineChars, - }); - } - - if (objectsParsed === 0) { - // If we're unable to parse the data and we have a decryption error, - // the error is probably in the decryption. - if (decryptError) { - throw decryptError; - } - - throw new DSMRParserError('Invalid telegram. No COSEM objects found.'); - } - - return result; -}; diff --git a/src/parsers/stream.ts b/src/parsers/stream.ts deleted file mode 100644 index 67515f5..0000000 --- a/src/parsers/stream.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PassThrough, Transform } from 'node:stream'; -import { - EncryptedDSMRStreamParser, - DSMRStreamParser as DSMRStreamParserType, - DSMRStreamParserOptions, -} from './stream-encrypted.js'; -import { UnencryptedDSMRStreamParser } from './stream-unencrypted.js'; - -/** - * Create a DSMR stream parser that reads data from a stream and calls a callback when a telegram is - * parsed. - * - * @param stream Stream to read data from - * @param options Settings for parsing the DSMR data - * @param callback Method that is called when a telegram is parsed or when an error occurred. - * @returns Method to stop the stream parser. - */ -export const createDSMRStreamParser = (options: DSMRStreamParserOptions): DSMRStreamParserType => { - if (options.decryptionKey) { - return new EncryptedDSMRStreamParser(options); - } - - return new UnencryptedDSMRStreamParser(options); -}; - -/** Create a DSMR stream transformer that reads data from a stream and pushes parsed telegrams. */ -export const createDSMRStreamTransformer = ( - options: Omit, -) => { - const passthrough = new PassThrough(); - - const transformer = new Transform({ - objectMode: true, - transform(chunk: Buffer, _encoding, callback) { - passthrough.write(chunk); - callback(); - }, - }); - - const parser = createDSMRStreamParser({ - ...options, - stream: passthrough, - callback: (error, result) => { - transformer.push({ error, result }); - }, - }); - - transformer.on('close', () => { - parser.destroy(); - }); - - return transformer; -}; diff --git a/src/protocols/cosem.ts b/src/protocols/cosem.ts new file mode 100644 index 0000000..a4a9071 --- /dev/null +++ b/src/protocols/cosem.ts @@ -0,0 +1,355 @@ +/** + * COSEM (Companion Specification for Energy Metering) defines (among other things) a way of + * identifying properties of smart meters using OBIS codes. These codes are used to identify the + * type of data being transmitted, and are used in the P1 port of smart meters. + */ + +import { SmartMeterParserResult } from '../index.js'; +import { + isEqualObisCode, + ObisCode, + ObisCodeString, + ObisCodeWildcard, + parseObisCodeWithWildcards, +} from './obis-code.js'; + +type ParameterTypes = 'number' | 'string' | 'raw'; + +export type DlmsCosemParameters = { + useDefaultScalar?: boolean; +}; + +export type DsmrCosemParameters = { + line: string; // The current line being parsed + lines: string[]; // All lines in the telegram + lineNumber: number; // The current line number +}; + +type BaseCallback = ( + opts: { + result: SmartMeterParserResult; + obisCode: ObisCode; + dlms?: DlmsCosemParameters; + dsmr?: DsmrCosemParameters; + } & T, +) => void; + +type CallbackString = BaseCallback<{ + valueString: string; +}>; + +type CallbackNumber = BaseCallback<{ + valueNumber: number; + valueString: string; + unit: string | null; +}>; + +type CallbackRaw = BaseCallback<{ + valueString: string; +}>; + +type Callback = T extends 'number' + ? CallbackNumber + : T extends 'string' + ? CallbackString + : T extends 'raw' + ? CallbackRaw + : never; + +class CosemLibraryInternal { + lib: ( + | { + obisCode: ObisCodeWildcard; + parameterType: 'string'; + callback: Callback<'string'>; + } + | { + obisCode: ObisCodeWildcard; + parameterType: 'number'; + callback: Callback<'number'>; + } + | { + obisCode: ObisCodeWildcard; + parameterType: 'raw'; + callback: Callback<'raw'>; + } + )[] = []; + + addStringParser(identifier: ObisCodeString, callback: CallbackString) { + const { obisCode } = parseObisCodeWithWildcards(identifier); + + if (!obisCode) throw new Error(`Invalid OBIS identifier: ${identifier}`); + + this.lib.push({ + parameterType: 'string', + obisCode, + callback, + }); + return this; + } + + addNumberParser(identifier: ObisCodeString, callback: CallbackNumber) { + const { obisCode } = parseObisCodeWithWildcards(identifier); + if (!obisCode) throw new Error(`Invalid OBIS identifier: ${identifier}`); + this.lib.push({ + parameterType: 'number', + obisCode, + callback, + }); + return this; + } + + addRawParser(identifier: ObisCodeString, callback: CallbackRaw) { + const { obisCode } = parseObisCodeWithWildcards(identifier); + + if (!obisCode) throw new Error(`Invalid OBIS identifier: ${identifier}`); + + this.lib.push({ + parameterType: 'raw', + obisCode, + callback, + }); + + return this; + } + + getParser(obisCode: ObisCode) { + return this.lib.find((item) => isEqualObisCode(item.obisCode, obisCode)); + } +} + +export const CosemLibrary = new CosemLibraryInternal() + .addNumberParser('1-3:0.2.8', ({ valueNumber, result }) => { + result.metadata.dsmrVersion = valueNumber / 10; + }) + .addStringParser('0-0:1.0.0', ({ valueString, result }) => { + result.metadata.timestamp = valueString; // TODO: Parse to date object + }) + .addStringParser('*-*:96.1.1', ({ valueString, result }) => { + result.metadata.equipmentId = valueString; + }) + .addNumberParser('1-*:1.8.*', ({ valueNumber, unit, obisCode, result }) => { + const tariff = obisCode.processing; + + if (unit?.toLowerCase() === 'kwh') { + valueNumber *= 1000; + } + + if (tariff === 0) { + result.electricity.total = result.electricity.total ?? {}; + result.electricity.total.received = valueNumber; + } else { + result.electricity.tariffs = result.electricity.tariffs ?? {}; + result.electricity.tariffs[tariff] = result.electricity.tariffs[tariff] ?? {}; + result.electricity.tariffs[tariff].received = valueNumber; + } + }) + .addNumberParser('1-*:2.8.*', ({ valueNumber, unit, obisCode, result }) => { + const tariff = obisCode.processing; + + if (unit?.toLowerCase() === 'kwh') { + valueNumber *= 1000; + } + + if (tariff === 0) { + result.electricity.total = result.electricity.total ?? {}; + result.electricity.total.returned = valueNumber; + } else { + result.electricity.tariffs = result.electricity.tariffs ?? {}; + result.electricity.tariffs[tariff] = result.electricity.tariffs[tariff] ?? {}; + result.electricity.tariffs[tariff].returned = valueNumber; + } + }) + .addNumberParser('0-0:96.14.0', ({ valueNumber, result }) => { + result.electricity.currentTariff = valueNumber; + }) + .addNumberParser('1-*:1.7.0', ({ valueNumber, unit, result }) => { + if (unit?.toLowerCase() === 'w') { + valueNumber *= 1000; + } + + result.electricity.powerReceivedTotal = valueNumber; + }) + .addNumberParser('1-*:2.7.0', ({ valueNumber, unit, result }) => { + if (unit?.toLowerCase() === 'w') { + valueNumber *= 1000; + } + + result.electricity.powerReturnedTotal = valueNumber; + }) + .addNumberParser('0-0:96.7.21', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.powerFailures = valueNumber; + }) + .addNumberParser('0-0:96.7.9', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.longPowerFailures = valueNumber; + }) + .addNumberParser('1-*:32.32.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; + result.metadata.events.voltageSags.l1 = valueNumber; + }) + .addNumberParser('1-*:52.32.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; + result.metadata.events.voltageSags.l2 = valueNumber; + }) + .addNumberParser('1-*:72.32.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSags = result.metadata.events.voltageSags ?? {}; + result.metadata.events.voltageSags.l3 = valueNumber; + }) + .addNumberParser('1-*:32.36.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; + result.metadata.events.voltageSwells.l1 = valueNumber; + }) + .addNumberParser('1-*:52.36.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; + result.metadata.events.voltageSwells.l2 = valueNumber; + }) + .addNumberParser('1-*:72.36.0', ({ valueNumber, result }) => { + result.metadata.events = result.metadata.events ?? {}; + result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; + result.metadata.events.voltageSwells.l3 = valueNumber; + }) + .addNumberParser('0-0:96.13.0', ({ valueString, result }) => { + result.metadata.textMessage = valueString; + }) + .addNumberParser('0-0:96.13.1', ({ valueNumber, result }) => { + result.metadata.numericMessage = valueNumber; + }) + .addNumberParser('1-*:32.7.0', ({ valueNumber, result }) => { + result.electricity.voltage = result.electricity.voltage ?? {}; + result.electricity.voltage.l1 = valueNumber; + }) + .addNumberParser('1-*:52.7.0', ({ valueNumber, result }) => { + result.electricity.voltage = result.electricity.voltage ?? {}; + result.electricity.voltage.l2 = valueNumber; + }) + .addNumberParser('1-*:72.7.0', ({ valueNumber, result }) => { + result.electricity.voltage = result.electricity.voltage ?? {}; + result.electricity.voltage.l3 = valueNumber; + }) + .addNumberParser('1-*:31.7.0', ({ valueNumber, result, dlms }) => { + // The currents for DLMS (in BasicList/DescribedList) mode are in 10 mA to + // give more precision without using floats. + if (dlms?.useDefaultScalar) { + valueNumber = valueNumber / 100; + } + + result.electricity.current = result.electricity.current ?? {}; + result.electricity.current.l1 = valueNumber; + }) + .addNumberParser('1-*:51.7.0', ({ valueNumber, result, dlms }) => { + if (dlms?.useDefaultScalar) { + valueNumber = valueNumber / 100; + } + + result.electricity.current = result.electricity.current ?? {}; + result.electricity.current.l2 = valueNumber; + }) + .addNumberParser('1-*:71.7.0', ({ valueNumber, result, dlms }) => { + if (dlms?.useDefaultScalar) { + valueNumber = valueNumber / 100; + } + + result.electricity.current = result.electricity.current ?? {}; + result.electricity.current.l3 = valueNumber; + }) + .addNumberParser('1-*:21.7.0', ({ valueNumber, result }) => { + result.electricity.powerReceived = result.electricity.powerReceived ?? {}; + result.electricity.powerReceived.l1 = valueNumber; + }) + .addNumberParser('1-*:41.7.0', ({ valueNumber, result }) => { + result.electricity.powerReceived = result.electricity.powerReceived ?? {}; + result.electricity.powerReceived.l2 = valueNumber; + }) + .addNumberParser('1-*:61.7.0', ({ valueNumber, result }) => { + result.electricity.powerReceived = result.electricity.powerReceived ?? {}; + result.electricity.powerReceived.l3 = valueNumber; + }) + .addNumberParser('1-*:22.7.0', ({ valueNumber, result }) => { + result.electricity.powerReturned = result.electricity.powerReturned ?? {}; + result.electricity.powerReturned.l1 = valueNumber; + }) + .addNumberParser('1-*:42.7.0', ({ valueNumber, result }) => { + result.electricity.powerReturned = result.electricity.powerReturned ?? {}; + result.electricity.powerReturned.l2 = valueNumber; + }) + .addNumberParser('1-*:62.7.0', ({ valueNumber, result }) => { + result.electricity.powerReturned = result.electricity.powerReturned ?? {}; + result.electricity.powerReturned.l3 = valueNumber; + }) + .addNumberParser('0-*:24.1.0', ({ valueNumber, result, obisCode }) => { + const busId = obisCode.channel; + const typeId = valueNumber; + + result.mBus[busId] = result.mBus[busId] ?? {}; + result.mBus[busId].deviceType = typeId; + }) + .addStringParser('0-*:96.1.0', ({ valueString, result, obisCode }) => { + const busId = obisCode.channel; + result.mBus[busId] = result.mBus[busId] ?? {}; + result.mBus[busId].equipmentId = valueString; + }) + .addRawParser('0-*:24.2.*', ({ valueString, result, obisCode }) => { + // Result is something like (101209112500W)(12785.123*m3) + const match = /^\(([^)]+)\)\(([\d.]+)\*(\w+)?\)/.exec(valueString); + + if (!match) { + return; + } + + const busId = obisCode.channel; + const timestamp = match[1]; + const mbusValue = parseFloat(match[2]); + const unit = match[3]; + + result.mBus[busId] = result.mBus[busId] ?? {}; + result.mBus[busId].timestamp = timestamp; // TODO: Parse to date object + result.mBus[busId].value = mbusValue; + result.mBus[busId].unit = unit; + }) + // Gas report for DSMR 3 meters. + // This is a bit off an odd one, as it's a two line parser. + // 0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3) + // (00000.000) + .addRawParser('0-*:24.3.0', ({ valueString, result, obisCode, dsmr }) => { + // This parser is not valid for DLMS (since it doesn't have the concept of line numbers). + if (!dsmr) return; + // Result is something like (090212160000)(00)(60)(1)(0-1:24.2.1)(m3) + const match = /^\((\w+)\)\((\d+)\)\((\d+)\)\((\d)\)\((0-\d:24\.2\.1)\)\((\w+)\)/.exec( + valueString, + ); + + if (!match) { + return; + } + + const nextLine = dsmr.lines[dsmr.lineNumber + 1]; + + if (!nextLine) { + return; + } + + const nextLineMatch = /^\(([\d.]+)\)/.exec(nextLine); + + if (!nextLineMatch) { + return; + } + + const busId = obisCode.channel; + const timestamp = match[1]; + const recordingPeriodMinutes = parseInt(match[3], 10); + const unit = match[6]; + const mbusValue = parseFloat(nextLineMatch[1]); + + result.mBus[busId] = result.mBus[busId] ?? {}; + result.mBus[busId].timestamp = timestamp; // TODO: Parse to date object + result.mBus[busId].value = mbusValue; + result.mBus[busId].unit = unit; + result.mBus[busId].recordingPeriodMinutes = recordingPeriodMinutes; + }); diff --git a/src/protocols/dlms-datatype.ts b/src/protocols/dlms-datatype.ts new file mode 100644 index 0000000..7eed202 --- /dev/null +++ b/src/protocols/dlms-datatype.ts @@ -0,0 +1,255 @@ +import { SmartMeterError } from '../util/errors.js'; +import { ObisCode, parseObisCodeFromBuffer } from './obis-code.js'; + +type DlmsDataTypeDecoder = (index: number, buffer: Buffer) => { value: T; index: number }; + +export type DlmsDataTypes = { + array: ParsedDlmsData[]; + structure: ParsedDlmsData[]; + octet_string: Buffer; + string: string; + uint8: number; + uint16: number; + uint32: number; + int8: number; + int16: number; + int32: number; + enum: number; + null: null; +}; + +export type ParsedDlmsData = { + value: DlmsDataTypes[TName]; + type: TName; +}; + +export const isParsedDlmsDataType = ( + name: TName, + value: ParsedDlmsData, +): value is ParsedDlmsData => { + return name === value.type; +}; + +export const isDlmsStructureLike = (object: ParsedDlmsData) => { + return isParsedDlmsDataType('structure', object) || isParsedDlmsDataType('array', object); +}; + +export const getDlmsNumberValue = (object: ParsedDlmsData) => { + return typeof object.value === 'number' ? object.value : null; +}; + +export const isDlmsObisCode = (object: ParsedDlmsData) => { + return isParsedDlmsDataType('octet_string', object) && object.value.length === 6; +}; + +export const getDlmsObisCode = (object: ParsedDlmsData): null | ObisCode => { + if (!isParsedDlmsDataType('octet_string', object)) { + return null; + } + + const { obisCode } = parseObisCodeFromBuffer(object.value); + + return obisCode; +}; + +export const getDlmsObjectCount = (data: Buffer, index: number) => { + let objectCount = data.readUint8(index++); + + if (objectCount > 0x80) { + if (objectCount === 0x81) { + objectCount = data.readUint8(index++); + } else if (objectCount === 0x82) { + objectCount = data.readUint16BE(index); + index += 2; + } else if (objectCount === 0x83) { + objectCount = data.readUint32BE(index); + index += 4; + } else { + throw new SmartMeterError(`Invalid object count 0x${objectCount.toString(16)}`); + } + } + + return { + objectCount, + newIndex: index, + }; +}; + +export type NestedStrings = string | NestedStrings[]; + +export function debugFriendlyDlmsDataType(object: ParsedDlmsData): NestedStrings { + if (Array.isArray(object.value)) { + return object.value.map(debugFriendlyDlmsDataType); + } + + if (Buffer.isBuffer(object.value)) { + return `${object.type}:${object.value.toString('hex')}`; + } + + return `${object.type}:${object.value}`; +} + +class DlmsDataTypesInternal { + parsers = new Map< + number, + { name: keyof DlmsDataTypes; parse: DlmsDataTypeDecoder } + >(); + + addDataType( + name: TName, + id: number, + parse: DlmsDataTypeDecoder, + ) { + this.parsers.set(id, { name, parse }); + return this; + } + + parse(buffer: Buffer, index: number): ParsedDlmsData & { index: number } { + if (index >= buffer.length) { + return { + value: null, + index, + type: 'null', + }; + } + + const dataType = buffer.readUint8(index++); + const parser = this.parsers.get(dataType); + + if (!parser) { + throw new SmartMeterError(`Unknown data type 0x${dataType.toString(16)}`); + } + + const { value, index: newIndex } = parser.parse(index, buffer); + index = newIndex; + + return { value, index, type: parser.name }; + } +} + +// TODO: We need to add all data types, because otherwise +// we will get an error when we try to parse a data type we don't know. +/** + * A DLMS data type is: + * + * - A tag + * - A Length (only for some data types) + * - The value (length is either determined by the tag and length) + */ +export const DlmsDataTypes = new DlmsDataTypesInternal() + .addDataType('array', 0x01, (index, buffer) => { + const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); + index = newIndex; + + const resultValue: DlmsDataTypes['array'] = []; + + for (let i = 0; i < objectCount; i++) { + const { value, index: newIndex, type } = DlmsDataTypes.parse(buffer, index); + index = newIndex; + resultValue.push({ + value, + type, + }); + } + + return { + index, + value: resultValue, + }; + }) + .addDataType('structure', 0x02, (index, buffer) => { + // TODO: This is the same as array + const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); + index = newIndex; + + const resultValue: DlmsDataTypes['array'] = []; + + for (let i = 0; i < objectCount; i++) { + const { value, index: newIndex, type } = DlmsDataTypes.parse(buffer, index); + index = newIndex; + resultValue.push({ + value, + type, + }); + } + + return { + index, + value: resultValue, + }; + }) + .addDataType('octet_string', 0x09, (index, buffer) => { + const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); + index = newIndex; + const value = buffer.subarray(index, index + objectCount); + index += objectCount; + return { + index, + value, + }; + }) + .addDataType('string', 0x0a, (index, buffer) => { + const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); + index = newIndex; + + const value = buffer.subarray(index, index + objectCount).toString('utf-8'); + + return { + index: index + objectCount, + value, + }; + }) + .addDataType('uint8', 0x11, (index, buffer) => { + const value = buffer.readUint8(index++); + return { + index, + value, + }; + }) + .addDataType('uint16', 0x12, (index, buffer) => { + const value = buffer.readUint16BE(index); + index += 2; + return { + index, + value, + }; + }) + .addDataType('uint32', 0x06, (index, buffer) => { + const value = buffer.readUint32BE(index); + index += 4; + return { + index, + value, + }; + }) + .addDataType('int8', 0x0f, (index, buffer) => { + const value = buffer.readInt8(index); + index += 1; + return { + index, + value, + }; + }) + .addDataType('int16', 0x10, (index, buffer) => { + const value = buffer.readInt16BE(index); + index += 2; + return { + index, + value, + }; + }) + .addDataType('int32', 0x05, (index, buffer) => { + const value = buffer.readInt32BE(index); + index += 4; + return { + index, + value, + }; + }) + .addDataType('enum', 0x16, (index, buffer) => { + const value = buffer.readUint8(index++); + return { + value, + index, + }; + }); diff --git a/src/protocols/dlms-payload/BasicList.ts b/src/protocols/dlms-payload/BasicList.ts new file mode 100644 index 0000000..aa73cb4 --- /dev/null +++ b/src/protocols/dlms-payload/BasicList.ts @@ -0,0 +1,75 @@ +import { + isDlmsStructureLike, + isParsedDlmsDataType, + isDlmsObisCode, + getDlmsObisCode, +} from '../dlms-datatype.js'; +import { addUnknownDlmsObject, makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; + +/** + * DLMS structure is like this: + * + * - Array + * - String: push list name + * - Octet_string (obis code) + * - Value (can be anything) + * - Octet_string (obis code) + * - Value (can be anything) + * - Etc... + */ +export const DlmsPayloadBasicList = makeDlmsPayload('BasicList', { + detector(dlms) { + if (!isDlmsStructureLike(dlms)) { + return false; + } + + const pushListName = dlms.value[0]; + + if (!isParsedDlmsDataType('string', pushListName)) { + return false; + } + + for (let i = 1; i < dlms.value.length; i += 2) { + const obisCode = dlms.value[i]; + + if (!isDlmsObisCode(obisCode)) { + return false; + } + } + + return true; + }, + parser(dlms, result) { + if (!isDlmsStructureLike(dlms)) { + return; + } + + for (let i = 1; i < dlms.value.length; i += 2) { + const obisCodeRaw = dlms.value[i]; + const valueRaw = dlms.value[i + 1]; + + const obisCode = getDlmsObisCode(obisCodeRaw); + + if (!obisCode) { + addUnknownDlmsObject(obisCodeRaw, result); + addUnknownDlmsObject(valueRaw, result); + continue; + } + + if (typeof valueRaw.value !== 'string' && typeof valueRaw.value !== 'number') { + addUnknownDlmsObject(valueRaw, result); + continue; + } + + parseDlmsCosem({ + result, + obisCode, + value: valueRaw.value, + unit: null, + dlms: { + useDefaultScalar: true, + }, + }); + } + }, +}); diff --git a/src/protocols/dlms-payload/BasicStructure.ts b/src/protocols/dlms-payload/BasicStructure.ts new file mode 100644 index 0000000..d298007 --- /dev/null +++ b/src/protocols/dlms-payload/BasicStructure.ts @@ -0,0 +1,172 @@ +import { + isDlmsStructureLike, + isDlmsObisCode, + getDlmsNumberValue, + getDlmsObisCode, + isParsedDlmsDataType, + ParsedDlmsData, +} from '../dlms-datatype.js'; +import { ObisCode } from '../obis-code.js'; +import { addUnknownDlmsObject, makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; + +export const DLMS_UNITS = { + [27]: 'W', + [28]: 'VA', + [29]: 'var', + [30]: 'Wh', + [31]: 'VAh', + [32]: 'varh', + [33]: 'A', + [34]: 'C', + [35]: 'V', +} as const; + +/** + * Cosem structure is an array/structure with 3 elements: + * + * 1. OBIS code (octet string) + * 2. Value (something else) + * 3. Unit (structure with 2 elements) + * + * - Scalar + * - Enum + */ +export const parseDlmsCosemStructure = (object: ParsedDlmsData) => { + if (!isParsedDlmsDataType('structure', object) && !isParsedDlmsDataType('array', object)) { + return null; + } + + if (object.value.length !== 3 && object.value.length !== 2) { + return null; + } + + let obisCode: ObisCode | null = null; + let value: string | number | null = null; + let unit: string | null = null; + let scalar = 1; + + for (const item of object.value) { + // This assumes that the first octet_string is the OBIS code. + if (!obisCode) { + const newObisCode = getDlmsObisCode(item); + + if (newObisCode) { + obisCode = newObisCode; + continue; + } + } + + if (isDlmsStructureLike(item)) { + if (item.value.length !== 2) continue; + + // This is the enum, it should contain the scalar value and the unit + for (const subItem of item.value) { + if (isParsedDlmsDataType('enum', subItem)) { + unit = DLMS_UNITS[subItem.value as keyof typeof DLMS_UNITS] ?? String(subItem.value); + } else { + const numberValue = getDlmsNumberValue(subItem); + + if (numberValue !== null) { + scalar = numberValue; + } + } + } + continue; + } + + if (isParsedDlmsDataType('string', item)) { + value = item.value; + continue; + } + + if (isParsedDlmsDataType('octet_string', item)) { + value = item.value.toString('hex'); + continue; + } + + const numberValue = getDlmsNumberValue(item); + + if (numberValue === null) continue; + + value = numberValue; + } + + if (typeof value === 'number' && scalar) { + value = Math.pow(10, scalar) * value; + } + + if (obisCode === null || value === null) { + return null; + } + + return { + obisCode, + value, + unit, + }; +}; + +/** + * Used by Aidon etc. + * + * DLMS structure is like this: + * + * - Array + * - Structure + * + * - Octet_string (obis code) + * - Value (can be anything) + * - Structure (optional, unit and scalar) + * + * - Int8 (scalar) + * - Enum (unit) + */ +export const DlmsPayloadBasicStructure = makeDlmsPayload('BasicStructure', { + detector: (dlms) => { + // Outer array and/or structure + if (!isDlmsStructureLike(dlms)) { + return false; + } + + for (const item of dlms.value) { + // Single Cosem item + if (!isDlmsStructureLike(item)) { + return false; + } + + if (item.value.length !== 3 && item.value.length !== 2) { + return false; + } + + if (!isDlmsObisCode(item.value[0])) { + return false; + } + } + + return true; + }, + parser: (dlms, result) => { + if (!isDlmsStructureLike(dlms)) { + return; + } + + for (const item of dlms.value) { + const cosemStructure = parseDlmsCosemStructure(item); + + if (!cosemStructure) { + addUnknownDlmsObject(item, result); + continue; + } + + parseDlmsCosem({ + obisCode: cosemStructure.obisCode, + value: cosemStructure.value, + unit: cosemStructure.unit, + dlms: { + useDefaultScalar: false, + }, + result, + }); + } + }, +}); diff --git a/src/protocols/dlms-payload/DescribedList.ts b/src/protocols/dlms-payload/DescribedList.ts new file mode 100644 index 0000000..cf4d992 --- /dev/null +++ b/src/protocols/dlms-payload/DescribedList.ts @@ -0,0 +1,70 @@ +import { getDlmsObisCode, isDlmsStructureLike } from '../dlms-datatype.js'; +import { makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; + +export const DlmsPayloadDescribedList = makeDlmsPayload('DescribedList', { + detector(dlms) { + if (!isDlmsStructureLike(dlms)) { + return false; + } + + const firstItem = dlms.value[0]; + + if (!isDlmsStructureLike(firstItem)) { + return false; + } + + if (firstItem.value.length !== dlms.value.length) { + return false; + } + + return true; + }, + parser(dlms, result) { + if (!isDlmsStructureLike(dlms)) { + return; + } + + const descriptorList = dlms.value[0]; + + if (!isDlmsStructureLike(descriptorList)) return; + + for (const [index, descriptor] of descriptorList.value.entries()) { + if (!isDlmsStructureLike(descriptor)) { + // TODO: Add unknown object. + continue; + } + + const obisRaw = descriptor.value[1]; + + if (!obisRaw) { + continue; + } + + const obisCode = getDlmsObisCode(obisRaw); + + if (!obisCode) { + continue; + } + + const valueRaw = dlms.value[index + 1]; + + if (!valueRaw) { + continue; + } + + if (typeof valueRaw.value !== 'string' && typeof valueRaw.value !== 'number') { + continue; + } + + parseDlmsCosem({ + obisCode, + value: valueRaw.value, + unit: null, + dlms: { + useDefaultScalar: true, + }, + result, + }); + } + }, +}); diff --git a/src/protocols/dlms-payload/dlms-payload.ts b/src/protocols/dlms-payload/dlms-payload.ts new file mode 100644 index 0000000..9270a60 --- /dev/null +++ b/src/protocols/dlms-payload/dlms-payload.ts @@ -0,0 +1,171 @@ +import { CosemLibrary, DlmsCosemParameters } from '../cosem.js'; +import { ParsedDlmsData } from '../dlms-datatype.js'; +import { HdlcParserResult } from '../hdlc.js'; +import { ObisCode, obisCodeToString } from '../obis-code.js'; +import type { parseDlmsCosemStructure } from './BasicStructure.js'; + +/** + * In DLMS, the payload can have different structures. Depending on the structure we have a + * different method of mapping the data to an OBIS code. + * + * @param name The name of the payload. This is used for debugging purposes and has no additional + * meaning. + * @param detector A function that detects if the payload is of this type. It should return true if + * the payload is of this type, and false otherwise. + * @param parser A function that parses the payload. It should take the payload and return the + * parsed data. + */ +export const makeDlmsPayload = ( + name: string, + { + detector, + parser, + }: { + detector: (dlms: ParsedDlmsData) => boolean; + parser: (dlms: ParsedDlmsData, result: HdlcParserResult) => void; + }, +) => { + return { name, detector, parser }; +}; + +export const parseDlmsCosem = ({ + obisCode, + value, + unit, + dlms, + result, +}: { + obisCode: ObisCode; + value: string | number; + unit: string | null; + dlms: DlmsCosemParameters; + result: HdlcParserResult; +}) => { + const parser = CosemLibrary.getParser(obisCode); + + const obisCodeString = obisCodeToString(obisCode); + const valueStr = `${value}${unit ? `*${unit}` : ''}`; + const cosemStr = `${obisCodeString}(${valueStr})`; + + if (!parser) { + result.cosem.unknownObjects.push(cosemStr); + return; + } + + switch (parser.parameterType) { + case 'number': { + if (typeof value !== 'number') { + result.cosem.unknownObjects.push(cosemStr); + return; + } + + parser.callback({ + result, + obisCode, + dlms, + valueNumber: value, + valueString: String(value), + unit: unit, + }); + + result.cosem.knownObjects.push(cosemStr); + break; + } + case 'string': { + if (typeof value !== 'string') { + result.cosem.unknownObjects.push(cosemStr); + return; + } + parser.callback({ + result, + obisCode, + dlms, + valueString: String(value), + }); + + result.cosem.knownObjects.push(cosemStr); + break; + } + case 'raw': { + if (typeof value !== 'string' && typeof value !== 'number') { + result.cosem.unknownObjects.push(cosemStr); + return; + } + + parser.callback({ + result, + obisCode, + dlms, + valueString: String(value), + }); + + result.cosem.knownObjects.push(cosemStr); + break; + } + } +}; + +export const addUnknownDlmsObject = ({ value, type }: ParsedDlmsData, result: HdlcParserResult) => { + let valueStr = ''; + + if (typeof value === 'string') { + valueStr = value; + } else if (typeof value === 'number') { + valueStr = String(value); + } else if (value === null) { + valueStr = 'null'; + } else if (Buffer.isBuffer(value)) { + valueStr = value.toString('hex'); + } else { + valueStr = JSON.stringify(value); + } + + result.dlms.unknownObjects.push(`${type}: ${valueStr}`); +}; + +export const addUnknownDlmsCosemObject = ( + obisCode: ObisCode, + { value }: ParsedDlmsData, + result: HdlcParserResult, +) => { + let valueStr = ''; + + if (typeof value === 'string') { + valueStr = value; + } else if (typeof value === 'number') { + valueStr = String(value); + } else if (value === null) { + valueStr = 'null'; + } else if (Buffer.isBuffer(value)) { + valueStr = value.toString('hex'); + } else { + valueStr = JSON.stringify(value); + } + + result.dlms.unknownObjects.push(`${obisCodeToString(obisCode)}(${valueStr})`); +}; + +export const addUnknownDlmsStructureObject = ( + structure: ReturnType, + result: HdlcParserResult, +) => { + if (!structure) return; + + let valueStr = ''; + + if (typeof structure.value === 'string') { + valueStr = structure.value; + } else if (typeof structure.value === 'number') { + valueStr = String(structure.value); + } else if (structure.value === null) { + valueStr = 'null'; + } else { + valueStr = JSON.stringify(structure.value); + } + + if (structure.unit) { + valueStr += `*${structure.unit}`; + } + + result.dlms.unknownObjects.push(`${obisCodeToString(structure.obisCode)}(${valueStr})`); +}; diff --git a/src/protocols/dlms-payload/dlms-payloads.ts b/src/protocols/dlms-payload/dlms-payloads.ts new file mode 100644 index 0000000..95b2a54 --- /dev/null +++ b/src/protocols/dlms-payload/dlms-payloads.ts @@ -0,0 +1,31 @@ +import { ParsedDlmsData } from '../dlms-datatype.js'; +import { HdlcParserResult } from '../hdlc.js'; +import { DlmsPayloadBasicList } from './BasicList.js'; +import { DlmsPayloadBasicStructure } from './BasicStructure.js'; +import { DlmsPayloadDescribedList } from './DescribedList.js'; +import type { makeDlmsPayload } from './dlms-payload.js'; + +class DlmsPayloadsInternal { + payloadDecoders: ReturnType[] = []; + + addPayload(payloadHandler: ReturnType) { + this.payloadDecoders.push(payloadHandler); + return this; + } + + parse(dlms: ParsedDlmsData, result: HdlcParserResult) { + for (const payload of this.payloadDecoders) { + if (payload.detector(dlms)) { + payload.parser(dlms, result); + return payload.name; + } + } + + throw new Error('No matching DLMS payload found'); + } +} + +export const DlmsPayloads = new DlmsPayloadsInternal() + .addPayload(DlmsPayloadBasicList) + .addPayload(DlmsPayloadBasicStructure) + .addPayload(DlmsPayloadDescribedList); diff --git a/src/protocols/dlms.ts b/src/protocols/dlms.ts new file mode 100644 index 0000000..b118b9c --- /dev/null +++ b/src/protocols/dlms.ts @@ -0,0 +1,107 @@ +/** + * DLMS (Device Language Message Specification) is a protocol used for communication with smart + * meters. + * + * We only support a specific subset of the DLMS protocol, which is used on the P1 port of smart + * meters. The entire protocol is much more complex and supports a wide range of message types. + * + * We support the following message types: + * + * - Data Notification (0x0f) + * - Encrypted Message (0xdb) with Data Notification messages inside. + * + * The contents of the DLMS Data Notification message is defined as follows: + * + * - Invoke id (4 bytes) + * - Timestamp (variable length) + * - Data + * + * The data is a DLMS structure, which uses a TLV-like encoding format. This format is implemented + * in {@link DlmsDataTypes}. + * + * You can find an online tool that can decode DLMS messages here: + * https://www.gurux.fi/GuruxDLMSTranslator + */ + +import { decryptDlmsFrame, ENCRYPTED_DLMS_TELEGRAM_SOF } from '../protocols/encryption.js'; +import { SmartMeterDecryptionRequired, SmartMeterUnknownMessageTypeError } from '../util/errors.js'; +import { DlmsDataTypes, getDlmsObjectCount } from './dlms-datatype.js'; +import { DlmsPayloads } from './dlms-payload/dlms-payloads.js'; +import { HdlcParserResult } from './hdlc.js'; + +export const DLMS_DATA_NOTIFICATION_SOF = 0x0f; + +export const decodeDlmsObis = ( + dlms: ReturnType, + result: HdlcParserResult, +) => { + const payloadType = DlmsPayloads.parse(dlms.data, result); + + result.dlms.payloadType = payloadType; +}; + +export const decodeDLMSContent = ({ + frame, + decryptionKey, + additionalAuthenticatedData, +}: { + frame: Buffer; + decryptionKey?: Buffer; + additionalAuthenticatedData?: Buffer; +}) => { + let index = 0; + const msgTypePeek = frame.readUint8(index); + let decryptionError: Error | undefined; + + if (msgTypePeek === ENCRYPTED_DLMS_TELEGRAM_SOF) { + if (!decryptionKey) { + throw new SmartMeterDecryptionRequired(); + } + + // Encrypted telegram + const { content, error } = decryptDlmsFrame({ + data: frame, + key: decryptionKey, + additionalAuthenticatedData, + }); + + decryptionError = error; + frame = content; + } + + try { + const msgType = frame.readUint8(index++); + + if (msgType !== DLMS_DATA_NOTIFICATION_SOF) { + throw new SmartMeterUnknownMessageTypeError(`Invalid message type 0x${msgType.toString(16)}`); + } + + const invokeId = frame.readUint32BE(index); + index += 4; + + const { objectCount: timeLength, newIndex } = getDlmsObjectCount(frame, index); + index = newIndex; + + const timestamp = + timeLength === 0x00 ? Buffer.alloc(0) : frame.subarray(index, index + timeLength); + + index += timeLength; + + const { value, type } = DlmsDataTypes.parse(frame, index); + + return { + invokeId, + timestamp, + data: { value, type }, + decryptionError, + }; + } catch (error) { + // If we're unable to parse the data and we have a decryption error, + // the error is probably in the decryption. + if (decryptionError) { + throw decryptionError; + } + + throw error; + } +}; diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts new file mode 100644 index 0000000..796eae9 --- /dev/null +++ b/src/protocols/dsmr.ts @@ -0,0 +1,277 @@ +import { decryptDlmsFrame } from './encryption.js'; +import { SmartMeterParserError } from '../util/errors.js'; +import { CosemLibrary } from './cosem.js'; +import { parseObisCodeFromString } from './obis-code.js'; +import { calculateCrc16Arc } from '../util/crc.js'; +import { BaseParserResult } from '../util/base-result.js'; + +export type DsmrParserOptions = + | { + /** Raw DSMR telegram */ + telegram: string; + /** Enable the encryption detection mechanism. Enabled by default */ + decryptionKey?: never; + additionalAuthenticatedData?: never; + encoding?: never; + } + | { + /** Encrypted DSMR telegram */ + telegram: Buffer; + /** Decryption key */ + decryptionKey?: Buffer; + /** AAD */ + additionalAuthenticatedData?: Buffer; + /** Encoding of the data in the buffer, defaults to binary */ + encoding?: BufferEncoding; + }; + +export type DsmrParserResult = BaseParserResult & { + dsmr: { + raw: string; + header: { + identifier: string; + xxx: string; + z: string; + }; + unknownLines?: string[]; + crc?: { + value: number; + valid: boolean; + }; + }; +}; + +/** Parses a string like "(1234.56*unit)", "(1234.56)", "(1234)" or "()". */ +const NumberTypeRegex = /^\(([\d.]+)?(\*\w+)?\)/; +/** Parses a string like "(string)". */ +const StringTypeRegex = /^\(([^)]+)?\)/; + +export const DSMR_SOF = 0x2f; // '/' +export const CR = 0x0d; // '\r' +export const LF = 0x0a; // '\n' +export const CRLF = '\r\n'; + +export const DEFAULT_FRAME_ENCODING = 'binary'; + +/** + * CRC is a CRC16 value calculated over the preceding characters in the data message (from “/” to + * “!” using the polynomial: x16+x15+x2+1). CRC16 uses no XOR in, no XOR out and is computed with + * least significant bit first. The value is represented as 4 hexadecimal characters (MSB first). + * + * @param telegram + * @param enteredCrc + * @returns + */ +export const isDsmrCrcValid = ({ + telegram, + crc, +}: { + telegram: string; + crc: number; +}) => { + // Strip the CRC from the telegram + const telegramParts = telegram.split(CRLF); + const strippedTelegram = telegramParts[0] + CRLF; + + const calculatedCrc = calculateCrc16Arc(Buffer.from(strippedTelegram, DEFAULT_FRAME_ENCODING)); + + return calculatedCrc === crc; +}; + +const decodeDsmrCosemLine = ({ + line, + lines, + lineNumber, + result, +}: { + line: string; + lines: string[]; + lineNumber: number; + result: DsmrParserResult; +}) => { + const { obisCode, consumedChars } = parseObisCodeFromString(line); + + if (obisCode === null) { + return false; + } + + const parser = CosemLibrary.getParser(obisCode); + + if (!parser) { + return false; + } + + const lineWithoutObisCode = line.slice(consumedChars, line.length); + + switch (parser.parameterType) { + case 'string': { + const regexResult = StringTypeRegex.exec(lineWithoutObisCode); + + if (!regexResult) { + return false; + } + + const valueString = regexResult[1] ?? ''; + + parser.callback({ + result, + obisCode, + valueString, + dsmr: { + line, + lines, + lineNumber, + }, + }); + return true; + } + case 'number': { + const regexResult = NumberTypeRegex.exec(lineWithoutObisCode); + + if (!regexResult) { + return false; + } + + const valueString = regexResult[1] ?? ''; + const unit = regexResult[2] ? regexResult[2].slice(1) : null; + const valueNumber = parseFloat(valueString); + + if (isNaN(valueNumber)) { + return false; + } + + parser.callback({ + result, + obisCode, + valueNumber, + valueString, + unit, + dsmr: { + line, + lines, + lineNumber, + }, + }); + + return true; + } + case 'raw': { + parser.callback({ + result, + obisCode, + valueString: lineWithoutObisCode, + dsmr: { + line, + lines, + lineNumber, + }, + }); + return true; + } + default: { + return false; + } + } +}; + +/** + * Parse a DSMR telegram into a structured object. + * + * @throws If CRC validation fails + */ +export const parseDsmr = (options: DsmrParserOptions): DsmrParserResult => { + let telegram: string; + let decryptError: Error | undefined; + + if (typeof options.telegram === 'string') { + telegram = options.telegram; + } else if (!Buffer.isBuffer(options.decryptionKey)) { + telegram = options.telegram.toString(options.encoding ?? DEFAULT_FRAME_ENCODING); + } else { + const { content, error } = decryptDlmsFrame({ + data: options.telegram, + key: options.decryptionKey, + additionalAuthenticatedData: options.additionalAuthenticatedData, + }); + + telegram = content.toString(options.encoding ?? DEFAULT_FRAME_ENCODING); + decryptError = error; + } + + const lines = telegram.split(CRLF); + + const result: DsmrParserResult = { + dsmr: { + raw: telegram, + header: { + identifier: '', + xxx: '', + z: '', + }, + }, + cosem: { + unknownObjects: [], + knownObjects: [], + }, + metadata: {}, + electricity: {}, + mBus: {}, + }; + + if (!result.dsmr) throw new Error('Invalid State.'); + + let objectsParsed = 0; + + for (const [lineNumber, line] of lines.entries()) { + if (line.startsWith('/')) { + // Beginning of telegram + result.dsmr.header.xxx = line.slice(1, 4); + result.dsmr.header.z = line.slice(4, 5); + result.dsmr.header.identifier = line.slice(5, line.length); + } else if (line.startsWith('!')) { + // End of telegram + if (line.length > 1) { + result.dsmr.crc = { + value: parseInt(line.slice(1, line.length), 16), + valid: false, + }; + } + } else if (line === '' || line === '\0') { + // skip empty lines + } else { + // Decode cosem object + const isLineParsed = decodeDsmrCosemLine({ + result, + line, + lines, + lineNumber, + }); + + if (!isLineParsed) { + result.dsmr.unknownLines = result.dsmr.unknownLines ?? []; + result.dsmr.unknownLines.push(line); + } else { + objectsParsed++; + } + } + } + + if (result.dsmr.crc !== undefined) { + result.dsmr.crc.valid = isDsmrCrcValid({ + telegram, + crc: result.dsmr.crc.value, + }); + } + + if (objectsParsed === 0) { + // If we're unable to parse the data and we have a decryption error, + // the error is probably in the decryption. + if (decryptError) { + throw decryptError; + } + + throw new SmartMeterParserError('Invalid telegram. No COSEM objects found.'); + } + + return result; +}; diff --git a/src/util/encryption.ts b/src/protocols/encryption.ts similarity index 51% rename from src/util/encryption.ts rename to src/protocols/encryption.ts index fb7617c..13f5186 100644 --- a/src/util/encryption.ts +++ b/src/protocols/encryption.ts @@ -1,9 +1,9 @@ import * as crypto from 'node:crypto'; -import { DSMRDecodeError, DSMRDecryptionError } from './errors.js'; +import { SmartMeterDecodeError, SmartMeterDecryptionError } from './../util/errors.js'; +import { getDlmsObjectCount } from './dlms-datatype.js'; /** - * For now this is specific to the luxembourg's smart metering system. (E-Meter P1 Specification) - * They wrap a DSMR telegram in a custom frame with the following format: + * Encrypted DSMR/DLMS frames have the following format: * * | Byte | Description | Example | * | ------ | -------------------- | ----------------------------------- | @@ -17,61 +17,58 @@ import { DSMRDecodeError, DSMRDecryptionError } from './errors.js'; * | 18-n | Frame | | * | n-n+12 | GCM Tag | 00 11 22 33 44 55 66 77 88 99 AA BB | * - * The encrypted DSMR frame is encrypted using AES-128-GCM, and the user can request the encryption - * key from the utility company. The IV is the concatenation of the system title and the frame - * counter. + * The encrypted DSMR/DLMS frame is encrypted using AES-128-GCM, and the user can request the + * encryption key from the utility company. The IV is the concatenation of the system title and the + * frame counter. * - * Length of frame is 17 (header length) + length of the encrypted DSMR frame. GCM tag length is - * excluded. + * Length of frame is 17 (header length, excluding sof) + length of the encrypted DSMR frame. GCM + * tag length is excluded. */ -export const ENCRYPTED_DSMR_TELEGRAM_SOF = 0xdb; // DLMS_COMMAND_GENERAL_GLO_CIPHERING -export const ENCRYPTED_DSMR_CONTENT_LENGTH_START = 0x82; // DLMS type for uint16_t (Big Endian) -export const ENCRYPTED_DSMR_SECURITY_TYPE = 0x30; // DLMS_SECURITY_AUTHENTICATION_ENCRYPTION -export const ENCRYPTED_DSMR_SYSTEM_TITLE_LEN = 8; -export const ENCRYPTED_DSMR_GCM_TAG_LEN = 12; -export const ENCRYPTED_DSMR_HEADER_LEN = 18; -export const ENCRYPTION_DEFAULT_AAD = Buffer.from('00112233445566778899aabbccddeeff', 'hex'); +export const ENCRYPTED_DLMS_TELEGRAM_SOF = 0xdb; // DLMS_COMMAND_GENERAL_GLO_CIPHERING +export const ENCRYPTED_DLMS_AUTHENTICATION_ENCRYPTION_TAG = 0x30; // DLMS_SECURITY_AUTHENTICATION_ENCRYPTION +export const ENCRYPTED_DLMS_ENCRYPTION_TAG = 0x20; // DLMS_SECURITY_ENCRYPTION +export const ENCRYPTED_DLMS_SYSTEM_TITLE_LEN = 8; +export const ENCRYPTED_DLMS_GCM_TAG_LEN = 12; +export const ENCRYPTED_DLMS_HEADER_LEN = 18; +export const ENCRYPTED_DLMS_DEFAULT_AAD = Buffer.from('00112233445566778899aabbccddeeff', 'hex'); /** * @param data A buffer that starts with the header (bytes 0-16) of the E-Meter P1 frame * @returns Decoded header */ -export const decodeHeader = (data: Buffer) => { - if (data.length < ENCRYPTED_DSMR_HEADER_LEN) { - throw new DSMRDecodeError('Invalid header length'); +export const decodeEncryptionHeader = (data: Buffer) => { + if (data.length < ENCRYPTED_DLMS_HEADER_LEN) { + throw new SmartMeterDecodeError('Invalid header length'); } let index = 0; const sof = data[index++]; - if (sof !== ENCRYPTED_DSMR_TELEGRAM_SOF) { - throw new DSMRDecodeError(`Invalid telegram sof 0x${sof.toString(16)}`); + if (sof !== ENCRYPTED_DLMS_TELEGRAM_SOF) { + throw new SmartMeterDecodeError(`Invalid telegram sof 0x${sof.toString(16)}`); } const systemTitleLen = data[index++]; - if (systemTitleLen !== ENCRYPTED_DSMR_SYSTEM_TITLE_LEN) { - throw new DSMRDecodeError(`Invalid system title length 0x${systemTitleLen.toString(16)}`); + if (systemTitleLen !== ENCRYPTED_DLMS_SYSTEM_TITLE_LEN) { + throw new SmartMeterDecodeError(`Invalid system title length 0x${systemTitleLen.toString(16)}`); } - const systemTitle = data.subarray(index, index + ENCRYPTED_DSMR_SYSTEM_TITLE_LEN); - index += ENCRYPTED_DSMR_SYSTEM_TITLE_LEN; + const systemTitle = data.subarray(index, index + ENCRYPTED_DLMS_SYSTEM_TITLE_LEN); + index += ENCRYPTED_DLMS_SYSTEM_TITLE_LEN; - const contentLengthStart = data[index++]; - if (contentLengthStart !== ENCRYPTED_DSMR_CONTENT_LENGTH_START) { - throw new DSMRDecodeError( - `Invalid content length start byte 0x${contentLengthStart.toString(16)}`, - ); - } + const { objectCount: frameLength, newIndex } = getDlmsObjectCount(data, index); + index = newIndex; - // The entire header is 18 bytes long, but for some reason the content length uses 17 as - // length for the header. Maybe they don't include the SOF byte? - const contentLength = data.readUInt16BE(index) + 1 - ENCRYPTED_DSMR_HEADER_LEN; - index += 2; + // The entire header is 18 bytes long, but the SOF is not included in the frame length. + const contentLength = frameLength + 1 - ENCRYPTED_DLMS_HEADER_LEN; const securityType = data[index++]; - if (securityType !== ENCRYPTED_DSMR_SECURITY_TYPE) { - throw new DSMRDecodeError(`Invalid frame counter 0x${securityType.toString(16)}`); + if ( + securityType !== ENCRYPTED_DLMS_AUTHENTICATION_ENCRYPTION_TAG && + securityType !== ENCRYPTED_DLMS_ENCRYPTION_TAG + ) { + throw new SmartMeterDecodeError(`Invalid security type 0x${securityType.toString(16)}`); } const frameCounter = data.subarray(index, index + 4); @@ -82,6 +79,7 @@ export const decodeHeader = (data: Buffer) => { frameCounter, securityType, contentLength, + consumedBytes: index, }; }; @@ -89,15 +87,18 @@ export const decodeHeader = (data: Buffer) => { * @param data A buffer that ends with the footer (bytes n-12 to n) of the E-Meter P1 frame * @returns Decoded footer */ -export const decodeFooter = (data: Buffer, header: ReturnType) => { - if (data.length < ENCRYPTED_DSMR_GCM_TAG_LEN) { - throw new DSMRDecodeError('Invalid footer length'); +export const decodeEncryptionFooter = ( + data: Buffer, + header: ReturnType, +) => { + if (data.length < ENCRYPTED_DLMS_GCM_TAG_LEN) { + throw new SmartMeterDecodeError('Invalid footer length'); } return { gcmTag: data.subarray( - ENCRYPTED_DSMR_HEADER_LEN + header.contentLength, - ENCRYPTED_DSMR_HEADER_LEN + header.contentLength + ENCRYPTED_DSMR_GCM_TAG_LEN, + header.consumedBytes + header.contentLength, + header.consumedBytes + header.contentLength + ENCRYPTED_DLMS_GCM_TAG_LEN, ), }; }; @@ -108,18 +109,16 @@ export const decryptFrameContents = ({ header, footer, key, - encoding, additionalAuthenticatedData, }: { /** The encrypted DSMR frame */ data: Buffer; - /** The decoded header (use {@link decodeHeader}) */ - header: ReturnType; - /** The decoded footer (use {@link decodeFooter}) */ - footer: ReturnType; + /** The decoded header (use {@link decodeEncryptionHeader}) */ + header: ReturnType; + /** The decoded footer (use {@link decodeEncryptionFooter}) */ + footer: ReturnType; /** The encryption key */ key: Buffer; - encoding: BufferEncoding; /** Optional additional authenticated data (AAD) to be used in the decryption. */ additionalAuthenticatedData?: Buffer; }) => { @@ -133,14 +132,14 @@ export const decryptFrameContents = ({ const iv = Buffer.concat([header.systemTitle, header.frameCounter]); let cipher: crypto.DecipherGCM; - let content = ''; + let content = Buffer.alloc(0); // 1: decrypt the frame, this will only throw if the key, iv or AAD are not // correct due to their format. `cipher.update` will never throw, but if the key/iv/aad // are not valid it may return gibberish. try { cipher = crypto.createDecipheriv('aes-128-gcm', key, iv, { - authTagLength: ENCRYPTED_DSMR_GCM_TAG_LEN, + authTagLength: ENCRYPTED_DLMS_GCM_TAG_LEN, }); cipher.setAutoPadding(false); cipher.setAuthTag(footer.gcmTag); @@ -149,11 +148,11 @@ export const decryptFrameContents = ({ cipher.setAAD(additionalAuthenticatedData); } - content += cipher.update(data, undefined, encoding); + content = Buffer.concat([content, cipher.update(data)]); } catch (error) { return { content, - error: new DSMRDecryptionError(error), + error: new SmartMeterDecryptionError(error), }; } @@ -161,11 +160,11 @@ export const decryptFrameContents = ({ // When either of these are invalid, it will throw an "Unsupported state or unable to authenticate data" error. // If the AAD is invalid, but the key/iv are valid the content can still be a valid DSMR frame! try { - content += cipher.final(encoding); + content = Buffer.concat([content, cipher.final()]); } catch (error) { return { content, - error: new DSMRDecryptionError(error), + error: new SmartMeterDecryptionError(error), }; } @@ -174,23 +173,21 @@ export const decryptFrameContents = ({ }; }; -/** Decrypts a full encrypted DSMR frame */ -export const decryptFrame = ({ +/** Decrypts a full encrypted DLMS frame */ +export const decryptDlmsFrame = ({ data, key, - encoding, additionalAuthenticatedData, }: { data: Buffer; key: Buffer; additionalAuthenticatedData?: Buffer; - encoding: BufferEncoding; }) => { - const header = decodeHeader(data); - const footer = decodeFooter(data, header); + const header = decodeEncryptionHeader(data); + const footer = decodeEncryptionFooter(data, header); const encryptedContent = data.subarray( - ENCRYPTED_DSMR_HEADER_LEN, - ENCRYPTED_DSMR_HEADER_LEN + header.contentLength, + header.consumedBytes, + header.consumedBytes + header.contentLength, ); const { content, error } = decryptFrameContents({ data: encryptedContent, @@ -198,7 +195,6 @@ export const decryptFrame = ({ footer, key, additionalAuthenticatedData, - encoding, }); return { diff --git a/src/protocols/hdlc.ts b/src/protocols/hdlc.ts new file mode 100644 index 0000000..b2c1b1c --- /dev/null +++ b/src/protocols/hdlc.ts @@ -0,0 +1,203 @@ +/** + * HDLC frame format: + * + * | Bytes | Description | Description | + * | ----- | ------------------- | --------------------------------------------------------------------- | + * | 1 | SOF | 0x7E (fixed) | + * | 2 | Format & Length | See "Format & Length" | + * | 1-4 | Destination Address | See "Addresses" | + * | 1-4 | Source Address | See "Addresses" | + * | 1 | Control | See "Control Byte" | + * | 2 | Header Checksum | CRC-16/IBM-SDLC of the header bytes (excluding SOF) | + * | n | Frame | Frame contents | + * | 2 | Frame Checksum | CRC-16/IBM-SDLC of the entire frame (including header, excluding SOF) | + * | 1 | EOF | 0x7E (fixed) | + * + * Format & Length: 0bTTTT_SLLL_LLLL_LLLL + * + * | Bits | Description | Description | + * | ---- | ------------ | -------------------------------------------------------------------------- | + * | T | Frame type | This implementation only supports frame type 0xA, which is fixed for dlms. | + * | S | Segmentation | When 1, the contents are split over multiple HDLC frames. | + * | L | Frame length | The length of the frame in bytes, excluding SOF/EOF. | + * + * Addresses: Addresses can be 1-4 bytes long, if the LSB is 1, the address is complete. If it is 0, + * the next byte is part of the address. The LSB of each byte is not part of the address. + * + * Control Byte: + * + * | Frame Type | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | + * | ----------- | ---- | ---- | ---- | --- | ---- | ---- | ---- | --- | + * | Information | N(R) | N(R) | N(R) | P/F | N(S) | N(S) | N(S) | 0 | + * | Supervisory | N(R) | N(R) | N(R) | P/F | N(S) | N(S) | 0 | 1 | + * | Unnumbered | N(R) | N(R) | N(R) | P/F | N(S) | N(S) | 1 | 1 | + * + * - N(R): Receive sequence number + * - N(S): Send sequence number + * - P/F: Poll/Final bit + * + * We only support unnumbered information frames (0b000P_0011), because we're relying on the meter + * to send unsolicited messages. + * + * The first three bytes in the frame, are an LLC header which in the the frames we're interested in + * always is: 0xe6, 0xe7, 0x00. The LLC header is not part of the HDLC frame, but is part of the + * frame contents. If the frame is segmented, the LLC header is only present in the first frame. + * + * You can find example HDLC frames in the documentation of Aidon's and Kamstrup's meters: + * + * - https://aidon.com/wp-content/uploads/2023/06/AIDONFD_RJ45_HAN_Interface_EN.pdf + * - https://kamstrup.com/-/media/kamstrup/downloads/technical-documentation/hdlc-telegrams.pdf + * + * These are included in the test suite of this library as well (see {@link tests/telegrams/dlms}) + */ + +import { BaseParserResult } from '../util/base-result.js'; +import { calculateCrc16IbmSdlc } from '../util/crc.js'; +import { SmartMeterError, SmartMeterUnknownMessageTypeError } from '../util/errors.js'; + +export type HdlcParserResult = BaseParserResult & { + hdlc: { + raw: string; + header: { + destinationAddress: number; + sourceAddress: number; + crc?: { + value: number; + valid: boolean; + }; + }; + crc?: { + value: number; + valid: boolean; + }; + }; + dlms: { + invokeId: number; + timestamp: string; // TODO make this a date object + unknownObjects: string[]; + payloadType: string; + }; +}; + +export const HDLC_TELEGRAM_SOF_EOF = 0x7e; +export const HDLC_FORMAT_START = 0xa; // HDLC format type 3 +export const HDLC_HEADER_LENGTH = 14; +export const HDLC_FOOTER_LENGTH = 3; +export const HDLC_LLC_HEADER_LENGTH = 3; +export const HDLC_LLC_DESTINATION = 0xe6; +export const HDLC_LLC_SOURCE = 0xe7; +export const HDLC_LLC_QUALITY = 0x00; + +const decodeHdlcAddress = (data: Buffer, index: number) => { + let i; + let address = 0; + + for (i = 0; i < 4; i++) { + const byte = data.readUint8(index + i); + + address = (address << 7) + ((byte & 0xfe) >> 1); + + if ((byte & 0b1) === 1) { + break; + } + } + + return { address, consumedBytes: i + 1 }; +}; + +export const decodeHdlcHeader = (data: Buffer) => { + if (data.length < HDLC_HEADER_LENGTH) { + throw new SmartMeterError('Invalid header length'); + } + + let index = 0; + + const sof = data.readUint8(index++); + + if (sof !== HDLC_TELEGRAM_SOF_EOF) { + throw new SmartMeterError(`Invalid telegram sof 0x${sof.toString(16)}`); + } + + const format = data.readUint8(index++); + const formatType = (format >> 4) & 0b1111; + // TODO: Is this bit "Segmentation supported", or "This frame is segmented"? + // The control bit also has a final bit, maybe that is used to indicate the end of a segmented frame? + const segmentation = (format & 0x08) !== 0; + + if (formatType !== HDLC_FORMAT_START) { + throw new SmartMeterError(`Invalid format type 0x${formatType.toString(16)}`); + } + + const frameLength = ((format & 0x07) << 8) + data.readUint8(index++); + + const { address: destinationAddress, consumedBytes: destinationAddressBytes } = decodeHdlcAddress( + data, + index, + ); + index += destinationAddressBytes; + + const { address: sourceAddress, consumedBytes: sourceAddressBytes } = decodeHdlcAddress( + data, + index, + ); + index += sourceAddressBytes; + + const controlByte = data.readUint8(index++); + + const calculatedCrc = calculateCrc16IbmSdlc(data.subarray(1, index)); + + const crc = data.readUint16LE(index); + index += 2; + + return { + formatType, + segmentation, + frameLength, + destinationAddress, + sourceAddress, + controlByte, + crc, + crcValid: crc === calculatedCrc, + consumedBytes: index, + }; +}; + +export const decodeLlcHeader = (frameContent: Buffer) => { + if (frameContent.length < HDLC_LLC_HEADER_LENGTH) { + throw new SmartMeterError('Invalid LLC header length'); + } + + const destination = frameContent.readUint8(0); + const source = frameContent.readUint8(1); + const quality = frameContent.readUint8(2); + + if (destination !== HDLC_LLC_DESTINATION) { + throw new SmartMeterUnknownMessageTypeError( + `Invalid LLC destination address 0x${destination.toString(16)}`, + ); + } + if (source !== HDLC_LLC_SOURCE) { + throw new SmartMeterUnknownMessageTypeError( + `Invalid LLC source address 0x${source.toString(16)}`, + ); + } + if (quality !== HDLC_LLC_QUALITY) { + throw new SmartMeterUnknownMessageTypeError(`Invalid LLC quality 0x${quality.toString(16)}`); + } + + return { destination, source, quality, consumedBytes: 3 }; +}; + +export const decodeHdlcFooter = (frame: Buffer) => { + if (frame[frame.length - 1] !== HDLC_TELEGRAM_SOF_EOF) { + throw new SmartMeterError(`Invalid footer eof 0x${frame[frame.length].toString(16)}`); + } + + const crc = frame.readUint16LE(frame.length - HDLC_FOOTER_LENGTH); + const calculatedCrc = calculateCrc16IbmSdlc(frame.subarray(1, -HDLC_FOOTER_LENGTH)); + + return { + crc, + crcValid: calculatedCrc === crc, + }; +}; diff --git a/src/protocols/obis-code.ts b/src/protocols/obis-code.ts new file mode 100644 index 0000000..7f8824f --- /dev/null +++ b/src/protocols/obis-code.ts @@ -0,0 +1,155 @@ +export const OBIS_WILDCARD = '*'; +type ObisWildCard = typeof OBIS_WILDCARD; +type ObisWildCardOrNumber = ObisWildCard | number; + +/** + * OBIS (Object Identification System) is a standard for identifying objects in energy meters and + * other devices. It uses a 6-byte code to represent different types of data, such as energy + * consumption, voltage, current, etc. The code is divided into six groups, each represented by a + * number. The first two groups are used to identify the type of data, while the last four groups + * are used to identify the specific data point. The code is usually represented in the format + * "A-B:C.D.E.F", + * + * In this case, we're not using the last byte (F) in our implementation, because DSMR skips it and + * for the DLSM/Cosem code it is always 0xff. + */ +export type ObisCodeString = + `${ObisWildCardOrNumber}-${ObisWildCardOrNumber}:${ObisWildCardOrNumber}.${ObisWildCardOrNumber}.${ObisWildCardOrNumber}`; +export type ObisCode = { + media: number; // Value group a + channel: number; // Value group b + physical: number; // Value group c + type: number; // Value group d + processing: number; // Value group e + history: number; // Value group f (not used in DSMR, and 0xff (N/A) in DLMS) +}; +export type ObisCodeWildcard = { + [key in keyof ObisCode]: ObisCode[key] | ObisWildCard; +}; + +/** Parses a string like "1-2:3.4.5" */ +const OBISIdentifierRegex = /^(\d{1,3})-(\d{1,3}):(\d{1,3})\.(\d{1,3})\.(\d{1,3})/; +/** Parses a string like "1-2:3.4.5", and allows using "*" as a wildcard. */ +const OBISIdentifierRegexWithWildcards = + /^(\d{1,3}|\*)-(\d{1,3}|\*):(\d{1,3}|\*)\.(\d{1,3}|\*)\.(\d{1,3}|\*)/; + +export const obisCodeToString = (obisCode: ObisCode): ObisCodeString => { + return `${obisCode.media}-${obisCode.channel}:${obisCode.physical}.${obisCode.type}.${obisCode.processing}`; +}; + +export const parseObisCodeFromString = ( + str: string, +): { + obisCode: ObisCode | null; + consumedChars: number; +} => { + const match = OBISIdentifierRegex.exec(str); + + if (!match) { + return { + obisCode: null, + consumedChars: 0, + }; + } + + return { + obisCode: { + media: parseInt(match[1], 10), + channel: parseInt(match[2], 10), + physical: parseInt(match[3], 10), + type: parseInt(match[4], 10), + processing: parseInt(match[5], 10), + history: 0xff, + }, + consumedChars: match[0].length, + }; +}; + +export const parseObisCodeFromBuffer = ( + buffer: Buffer, +): { + obisCode: ObisCode | null; +} => { + if (buffer.length !== 6) { + return { obisCode: null }; + } + + return { + obisCode: { + media: buffer[0], + channel: buffer[1], + physical: buffer[2], + type: buffer[3], + processing: buffer[4], + history: buffer[5], + }, + }; +}; + +export const parseObisCodeWithWildcards = ( + str: string, +): { + obisCode: ObisCodeWildcard | null; + consumedChars: number; +} => { + const match = OBISIdentifierRegexWithWildcards.exec(str); + + if (!match) { + return { + obisCode: null, + consumedChars: 0, + }; + } + + return { + obisCode: { + media: match[1] === OBIS_WILDCARD ? OBIS_WILDCARD : parseInt(match[1], 10), + channel: match[2] === OBIS_WILDCARD ? OBIS_WILDCARD : parseInt(match[2], 10), + physical: match[3] === OBIS_WILDCARD ? OBIS_WILDCARD : parseInt(match[3], 10), + type: match[4] === OBIS_WILDCARD ? OBIS_WILDCARD : parseInt(match[4], 10), + processing: match[5] === OBIS_WILDCARD ? OBIS_WILDCARD : parseInt(match[5], 10), + history: OBIS_WILDCARD, + }, + consumedChars: match[0].length, + }; +}; + +export const isEqualObisCode = (codeA: ObisCodeWildcard, codeB: ObisCodeWildcard) => { + if ( + codeA.media !== OBIS_WILDCARD && + codeB.media !== OBIS_WILDCARD && + codeA.media !== codeB.media + ) { + return false; + } + + if ( + codeA.channel !== OBIS_WILDCARD && + codeB.channel !== OBIS_WILDCARD && + codeA.channel !== codeB.channel + ) { + return false; + } + + if ( + codeA.physical !== OBIS_WILDCARD && + codeB.physical !== OBIS_WILDCARD && + codeA.physical !== codeB.physical + ) { + return false; + } + + if (codeA.type !== OBIS_WILDCARD && codeB.type !== OBIS_WILDCARD && codeA.type !== codeB.type) { + return false; + } + + if ( + codeA.processing !== OBIS_WILDCARD && + codeB.processing !== OBIS_WILDCARD && + codeA.processing !== codeB.processing + ) { + return false; + } + + return true; +}; diff --git a/src/stream/stream-detect-type.ts b/src/stream/stream-detect-type.ts new file mode 100644 index 0000000..4534404 --- /dev/null +++ b/src/stream/stream-detect-type.ts @@ -0,0 +1,221 @@ +import { isAscii } from 'node:buffer'; +import { Readable } from 'node:stream'; + +import { + decodeEncryptionHeader, + ENCRYPTED_DLMS_HEADER_LEN, + ENCRYPTED_DLMS_TELEGRAM_SOF, +} from '../protocols/encryption.js'; +import { + decodeHdlcHeader, + decodeLlcHeader, + HDLC_HEADER_LENGTH, + HDLC_LLC_HEADER_LENGTH, + HDLC_TELEGRAM_SOF_EOF, +} from '../protocols/hdlc.js'; +import { CR, DSMR_SOF, LF } from '../protocols/dsmr.js'; +import { SmartMeterStreamParser } from './stream.js'; + +type StreamDetectTypeCallback = (result: { + mode: 'dsmr' | 'dlms'; + encrypted: boolean; + /** Note that the frame might not start at the beginning of the buffer. */ + data: Buffer; +}) => void; + +/** This class detects the type of stream (DSMR or DLMS) and whether it is encrypted or not. */ +export class StreamDetectType implements SmartMeterStreamParser { + public readonly startOfFrameByte = DSMR_SOF; + + private boundOnData: StreamDetectType['onData']; + private telegram = Buffer.alloc(0); + + constructor( + private options: { + stream: Readable; + callback: StreamDetectTypeCallback; + }, + ) { + this.boundOnData = this.onData.bind(this); + options.stream.addListener('data', this.boundOnData); + } + + private onData(data: Buffer) { + this.telegram = Buffer.concat([this.telegram, data]); + + const { hasFoundDsmr, canClearDsmr } = this.onDataCheckDSMR(); + + if (hasFoundDsmr) { + this.options.callback({ + mode: 'dsmr', + encrypted: false, + data: this.telegram, + }); + this.clear(); + return; + } + + // Note: It is important to check for DLMS before checking for encrypted DSMR, + // as the encryption headers are the same for both encrypted DSMR and DLMS. + const { hasFoundDlms, encryptedDlms, canClearDlms } = this.onDataCheckDLMS(); + + if (hasFoundDlms) { + this.options.callback({ + mode: 'dlms', + encrypted: encryptedDlms ?? false, + data: this.telegram, + }); + this.clear(); + return; + } + + const { hasFoundEncryptedDsmr, canClearEncryptedDsmr } = this.onDataCheckEncryptedDSMR(); + + if (hasFoundEncryptedDsmr) { + this.options.callback({ + mode: 'dsmr', + encrypted: true, + data: this.telegram, + }); + this.clear(); + return; + } + + // If all three checks are not finding valid telegrams, and they are not + // waiting for more data, we can clear the telegram buffer. + if (canClearDsmr && canClearDlms && canClearEncryptedDsmr) { + this.clear(); + } + } + + private onDataCheckDSMR() { + // If the telegram is not ascii, it cannot be a DSMR telegram. + // IsAscii is guaranteed to be false for DLMS/encrypted telegrams, as they have SOFs that are not + // in the ascii range. + if (!isAscii(this.telegram)) { + return { + hasFoundDsmr: false, + canClearDsmr: true, + }; + } + + const sofIndex = this.telegram.indexOf(DSMR_SOF); + if (sofIndex === -1) { + return { + hasFoundDsmr: false, + canClearDsmr: true, + }; + } + + const carriageReturnIndex = this.telegram.indexOf(CR, sofIndex + 1); + + if (carriageReturnIndex === -1) { + return { + hasFoundDsmr: false, + canClearDsmr: false, + }; + } + + const minimumTelegramLength = carriageReturnIndex + 1; + if (this.telegram.length <= minimumTelegramLength) { + return { + hasFoundDsmr: false, + canClearDsmr: false, + }; + } + + const newLineCheck = this.telegram[carriageReturnIndex + 1]; + + // If we haven't found a new line, we can clear the telegram buffer as + // it is not a valid DSMR telegram. + return { + hasFoundDsmr: newLineCheck === LF, + canClearDsmr: true, + }; + } + + private onDataCheckDLMS() { + const sofIndex = this.telegram.indexOf(HDLC_TELEGRAM_SOF_EOF); + + if (sofIndex === -1) { + return { + hasFoundDlms: false, + canClearDlms: true, + }; + } + + const minimumTelegramLength = sofIndex + HDLC_HEADER_LENGTH; + + if (this.telegram.length < minimumTelegramLength) { + return { + hasFoundDlms: false, + canClearDlms: false, + }; + } + + try { + const header = decodeHdlcHeader(this.telegram.subarray(sofIndex, this.telegram.length)); + decodeLlcHeader(this.telegram.subarray(sofIndex + header.consumedBytes)); + + const contentStart = this.telegram.readUint8( + sofIndex + header.consumedBytes + HDLC_LLC_HEADER_LENGTH, + ); + + return { + hasFoundDlms: true, + canClearDlms: true, + encryptedDlms: contentStart === ENCRYPTED_DLMS_TELEGRAM_SOF, + }; + } catch (_error) { + return { hasFoundDlms: false }; + } + } + + private onDataCheckEncryptedDSMR() { + const sofIndex = this.telegram.indexOf(ENCRYPTED_DLMS_TELEGRAM_SOF); + + if (sofIndex === -1) { + return { + hasFoundEncryptedDsmr: false, + canClearEncryptedDsmr: true, + }; + } + + const minimumTelegramLength = sofIndex + ENCRYPTED_DLMS_HEADER_LEN; + + if (this.telegram.length < minimumTelegramLength) { + return { + hasFoundEncryptedDsmr: false, + canClearEncryptedDsmr: false, + }; + } + + try { + // Frame is encrypted when the header can be successfully decoded. + decodeEncryptionHeader(this.telegram.subarray(sofIndex)); + + return { + hasFoundEncryptedDsmr: true, + canClearEncryptedDsmr: true, + }; + } catch (_error) { + return { + hasFoundEncryptedDsmr: false, + canClearEncryptedDsmr: false, + }; + } + } + + destroy() { + this.clear(); + this.options.stream.removeListener('data', this.boundOnData); + } + + clear() { + this.telegram = Buffer.alloc(0); + } + + currentSize() { + return this.telegram.length; + } +} diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts new file mode 100644 index 0000000..19c6adb --- /dev/null +++ b/src/stream/stream-dlms.ts @@ -0,0 +1,204 @@ +/* eslint-disable no-console */ + +import { Readable } from 'stream'; +import { + decodeHdlcFooter, + decodeHdlcHeader, + decodeLlcHeader, + HDLC_FOOTER_LENGTH, + HDLC_HEADER_LENGTH, + HDLC_TELEGRAM_SOF_EOF, + HdlcParserResult, +} from './../protocols/hdlc.js'; +import { SmartMeterError, StartOfFrameNotFoundError } from '../util/errors.js'; +import { decodeDLMSContent, decodeDlmsObis } from './../protocols/dlms.js'; +import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; + +export type DlmsStreamParserOptions = { + stream: Readable; + callback: SmartMeterStreamCallback; + /** Decryption key */ + decryptionKey?: Buffer; + /** AAD */ + additionalAuthenticatedData?: Buffer; +}; + +export class DlmsStreamParser implements SmartMeterStreamParser { + public readonly startOfFrameByte = HDLC_TELEGRAM_SOF_EOF; + + private hasStartOfFrame = false; + private telegram = Buffer.alloc(0); + private cachedContent = Buffer.alloc(0); + private header: ReturnType | undefined = undefined; + + private readonly boundOnData = this.onData.bind(this); + + constructor(private options: DlmsStreamParserOptions) { + this.options.stream.addListener('data', this.boundOnData); + } + + private onData(data: Buffer) { + if (!this.hasStartOfFrame) { + const sofIndex = data.indexOf(HDLC_TELEGRAM_SOF_EOF); + + if (sofIndex === -1) { + const error = new StartOfFrameNotFoundError(); + error.withRawTelegram(data); + + this.options.callback(error, undefined); + } + + this.telegram = data.subarray(sofIndex, data.length); + this.hasStartOfFrame = true; + } else { + this.telegram = Buffer.concat([this.telegram, data]); + } + + if (this.header === undefined && this.telegram.length >= HDLC_HEADER_LENGTH) { + try { + this.header = decodeHdlcHeader(this.telegram); + } catch (error) { + this.clear(); + + if (error instanceof SmartMeterError) { + error.withRawTelegram(this.telegram); + } + + this.options.callback(error, undefined); + + const remainingData = this.telegram.subarray(1, this.telegram.length); + this.hasStartOfFrame = false; + this.header = undefined; + this.telegram = Buffer.alloc(0); + this.cachedContent = Buffer.alloc(0); + + // There might be more data in the buffer for the next telegram. + if (remainingData.length > 0) { + this.onData(remainingData); + } + return; + } + } + + // Wait for more data to decode the header + if (!this.header) return; + const totalLength = this.header.frameLength + 2; // 2 bytes for the sof and eof. + + if (this.telegram.length < totalLength) { + return; // Wait for more data + } + + if (this.header.segmentation) { + // This frame is not complete yet, wait for more data. + // TODO: Parse the footer and check the crc. + this.cachedContent = Buffer.concat([ + this.cachedContent, + this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH), + ]); + + const remainingData = this.telegram.subarray(totalLength, this.telegram.length); + this.hasStartOfFrame = false; + this.header = undefined; + this.telegram = Buffer.alloc(0); + + // There might be more data in the buffer for the next telegram. + if (remainingData.length > 0) { + this.onData(remainingData); + } + + return; + } + + try { + const content = Buffer.concat([ + this.cachedContent, + this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH), + ]); // Last two bytes of content are the footer + + const llc = decodeLlcHeader(content); + + const completeTelegram = this.telegram.subarray(0, totalLength); + + const footer = decodeHdlcFooter(completeTelegram); + + const dlmsContent = decodeDLMSContent({ + frame: content.subarray(llc.consumedBytes), + decryptionKey: this.options.decryptionKey, + additionalAuthenticatedData: this.options.additionalAuthenticatedData, + }); + + const result: HdlcParserResult = { + hdlc: { + raw: completeTelegram.toString('hex'), + header: { + destinationAddress: this.header.destinationAddress, + sourceAddress: this.header.sourceAddress, + crc: { + value: this.header.crc, + valid: this.header.crcValid, + }, + }, + crc: { + value: footer.crc, + valid: footer.crcValid, + }, + }, + // DLMS properties will be filled in by `decodeDlmsObis` + dlms: { + invokeId: 0, + timestamp: '', + unknownObjects: [], + payloadType: '', + }, + cosem: { + unknownObjects: [], + knownObjects: [], + }, + electricity: {}, + mBus: {}, + metadata: {}, + }; + + if (this.options.decryptionKey) { + result.additionalAuthenticatedDataValid = dlmsContent.decryptionError === undefined; + } + + decodeDlmsObis(dlmsContent, result); + + this.options.callback(null, result); + } catch (error) { + if (error instanceof SmartMeterError) { + error.withRawTelegram(this.telegram); + } + + this.options.callback(error, undefined); + } + + const remainingData = this.telegram.subarray(totalLength, this.telegram.length); + this.hasStartOfFrame = false; + this.header = undefined; + this.telegram = Buffer.alloc(0); + this.cachedContent = Buffer.alloc(0); + + // There might be more data in the buffer for the next telegram. + if (remainingData.length > 0) { + this.onData(remainingData); + } + } + + destroy(): void { + this.options.stream.removeListener('data', this.boundOnData); + this.clear(); + } + + clear(): void { + this.hasStartOfFrame = false; + this.header = undefined; + this.telegram = Buffer.alloc(0); + this.cachedContent = Buffer.alloc(0); + } + + currentSize(): number { + throw new Error('Method not implemented.'); + } +} diff --git a/src/parsers/stream-encrypted.ts b/src/stream/stream-encrypted-dsmr.ts similarity index 66% rename from src/parsers/stream-encrypted.ts rename to src/stream/stream-encrypted-dsmr.ts index 102e5f3..915f840 100644 --- a/src/parsers/stream-encrypted.ts +++ b/src/stream/stream-encrypted-dsmr.ts @@ -1,35 +1,25 @@ import { Readable } from 'node:stream'; -import type { DSMRParserOptions, DSMRParserResult } from '../index.js'; import { - decodeFooter, - decodeHeader, + decodeEncryptionFooter, + decodeEncryptionHeader, decryptFrameContents, - ENCRYPTED_DSMR_GCM_TAG_LEN, - ENCRYPTED_DSMR_HEADER_LEN, - ENCRYPTED_DSMR_TELEGRAM_SOF, -} from '../util/encryption.js'; -import { DEFAULT_FRAME_ENCODING } from '../util/frame-validation.js'; -import { DSMRParser } from './dsmr.js'; -import { DSMRError, DSMRStartOfFrameNotFoundError, DSMRTimeoutError } from '../util/errors.js'; - -export type DSMRStreamParser = { - /** Stop the stream parser. */ - destroy(): void; - /** Clear all cached data */ - clear(): void; - /** Size in bytes of the data that is cached */ - currentSize(): number; - /** The byte that indicates a start of frame was found for this parser */ - readonly startOfFrameByte: number; -}; + ENCRYPTED_DLMS_GCM_TAG_LEN, + ENCRYPTED_DLMS_HEADER_LEN, + ENCRYPTED_DLMS_TELEGRAM_SOF, +} from '../protocols/encryption.js'; +import { DsmrParserOptions, DsmrParserResult, parseDsmr } from './../protocols/dsmr.js'; +import { + SmartMeterError, + StartOfFrameNotFoundError, + SmartMeterTimeoutError, +} from '../util/errors.js'; +import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; -export type DSMRStreamParserOptions = Omit & { +export type DSMRStreamParserOptions = Omit & { /** The stream which is going to provide the data */ stream: Readable; /** The callback that will be called when a telegram was parsed. */ - callback: DSMRStreamCallback; - /** Should the non-encrypted mode try to detect if the frame that is received is encrypted? */ - detectEncryption?: boolean; + callback: SmartMeterStreamCallback; /** * Maximum time in milliseconds to wait for a full frame to be received. The timer starts when a * valid start of frame/header is received. @@ -37,18 +27,16 @@ export type DSMRStreamParserOptions = Omit & { fullFrameRequiredWithinMs?: number; }; -export type DSMRStreamCallback = (error: unknown, result?: DSMRParserResult) => void; - -export class EncryptedDSMRStreamParser implements DSMRStreamParser { +export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { private hasStartOfFrame = false; - private header: ReturnType | undefined = undefined; + private header: ReturnType | undefined = undefined; private telegram = Buffer.alloc(0); private fullFrameRequiredWithinMs: number; private fullFrameRequiredTimeout?: NodeJS.Timeout; private boundOnData: EncryptedDSMRStreamParser['onData']; private boundOnFullFrameRequiredTimeout: EncryptedDSMRStreamParser['onFullFrameRequiredTimeout']; - public readonly startOfFrameByte = ENCRYPTED_DSMR_TELEGRAM_SOF; + public readonly startOfFrameByte = ENCRYPTED_DLMS_TELEGRAM_SOF; constructor(private options: DSMRStreamParserOptions) { this.boundOnData = this.onData.bind(this); @@ -60,11 +48,11 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { private onData(data: Buffer) { if (!this.hasStartOfFrame) { - const sofIndex = data.indexOf(ENCRYPTED_DSMR_TELEGRAM_SOF); + const sofIndex = data.indexOf(ENCRYPTED_DLMS_TELEGRAM_SOF); // Not yet a valid frame. Discard the data if (sofIndex === -1) { - const error = new DSMRStartOfFrameNotFoundError(); + const error = new StartOfFrameNotFoundError(); error.withRawTelegram(data); this.options.callback(error, undefined); @@ -81,13 +69,13 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { this.telegram = Buffer.concat([this.telegram, data]); } - if (this.header === undefined && this.telegram.length >= ENCRYPTED_DSMR_HEADER_LEN) { + if (this.header === undefined && this.telegram.length >= ENCRYPTED_DLMS_HEADER_LEN) { try { - this.header = decodeHeader(this.telegram); + this.header = decodeEncryptionHeader(this.telegram); } catch (error) { this.clear(); - if (error instanceof DSMRError) { + if (error instanceof SmartMeterError) { error.withRawTelegram(this.telegram); } @@ -100,7 +88,7 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { if (!this.header) return; const totalLength = - ENCRYPTED_DSMR_HEADER_LEN + this.header.contentLength + ENCRYPTED_DSMR_GCM_TAG_LEN; + ENCRYPTED_DLMS_HEADER_LEN + this.header.contentLength + ENCRYPTED_DLMS_GCM_TAG_LEN; // Wait until full telegram is received if (this.telegram.length < totalLength) return; @@ -111,10 +99,10 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { try { const encryptedContent = this.telegram.subarray( - ENCRYPTED_DSMR_HEADER_LEN, - ENCRYPTED_DSMR_HEADER_LEN + this.header.contentLength, + ENCRYPTED_DLMS_HEADER_LEN, + ENCRYPTED_DLMS_HEADER_LEN + this.header.contentLength, ); - const footer = decodeFooter(this.telegram, this.header); + const footer = decodeEncryptionFooter(this.telegram, this.header); const { content, error } = decryptFrameContents({ data: encryptedContent, @@ -122,14 +110,12 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { footer, key: this.options.decryptionKey ?? Buffer.alloc(0), additionalAuthenticatedData: this.options.additionalAuthenticatedData, - encoding: this.options.encoding ?? DEFAULT_FRAME_ENCODING, }); decryptError = error; - const result = DSMRParser({ + const result = parseDsmr({ telegram: content, - newLineChars: this.options.newLineChars, }); result.additionalAuthenticatedDataValid = decryptError === undefined; @@ -140,7 +126,7 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { // So that should be returned to the listener. const realError = decryptError ?? error; - if (realError instanceof DSMRError) { + if (realError instanceof SmartMeterError) { realError.withRawTelegram(this.telegram); } @@ -160,7 +146,7 @@ export class EncryptedDSMRStreamParser implements DSMRStreamParser { } private onFullFrameRequiredTimeout() { - const error = new DSMRTimeoutError(); + const error = new SmartMeterTimeoutError(); error.withRawTelegram(this.telegram); this.options.callback(error, undefined); diff --git a/src/parsers/stream-unencrypted.ts b/src/stream/stream-unencrypted-dsmr.ts similarity index 57% rename from src/parsers/stream-unencrypted.ts rename to src/stream/stream-unencrypted-dsmr.ts index ab5a5a9..b670de5 100644 --- a/src/parsers/stream-unencrypted.ts +++ b/src/stream/stream-unencrypted-dsmr.ts @@ -1,26 +1,18 @@ -import { DSMRStreamParser, DSMRStreamParserOptions } from './stream-encrypted.js'; -import { DSMRParser } from './dsmr.js'; +import { DSMRStreamParserOptions } from './stream-encrypted-dsmr.js'; +import { CR, DEFAULT_FRAME_ENCODING, LF, parseDsmr } from './../protocols/dsmr.js'; import { - DEFAULT_FRAME_ENCODING, - isAsciiFrame, - isEncryptedFrame, -} from '../util/frame-validation.js'; -import { - DSMRDecodeError, - DSMRDecryptionRequired, - DSMRError, - DSMRStartOfFrameNotFoundError, - DSMRTimeoutError, + SmartMeterError, + StartOfFrameNotFoundError, + SmartMeterTimeoutError, } from '../util/errors.js'; -import { ENCRYPTED_DSMR_HEADER_LEN, ENCRYPTED_DSMR_TELEGRAM_SOF } from '../util/encryption.js'; +import { SmartMeterStreamParser } from './stream.js'; -export class UnencryptedDSMRStreamParser implements DSMRStreamParser { +export class UnencryptedDSMRStreamParser implements SmartMeterStreamParser { private telegram: Buffer = Buffer.alloc(0); private hasStartOfFrame = false; private eofRegex: RegExp; private boundOnData: UnencryptedDSMRStreamParser['onData']; private boundOnFullFrameRequiredTimeout: UnencryptedDSMRStreamParser['onFullFrameRequiredTimeout']; - private detectEncryption: boolean; private encoding: BufferEncoding; private fullFrameRequiredTimeoutMs: number; private fullFrameRequiredTimeout?: NodeJS.Timeout; @@ -32,51 +24,23 @@ export class UnencryptedDSMRStreamParser implements DSMRStreamParser { this.boundOnFullFrameRequiredTimeout = this.onFullFrameRequiredTimeout.bind(this); this.options.stream.addListener('data', this.boundOnData); - this.detectEncryption = options.detectEncryption ?? true; this.encoding = options.encoding ?? DEFAULT_FRAME_ENCODING; this.fullFrameRequiredTimeoutMs = options.fullFrameRequiredWithinMs ?? 5000; // End of frame is \r\n!\r\n with the CRC being optional as // it is only for DSMR 4 and up. - this.eofRegex = - options.newLineChars === '\n' ? /\n!([0-9A-Fa-f]+)?\n(\0)?/ : /\r\n!([0-9A-Fa-f]+)?\r\n(\0)?/; + this.eofRegex = /\r\n!([0-9A-Fa-f]{4})?\r\n(\0)?/; } private onData(dataRaw: Buffer) { this.telegram = Buffer.concat([this.telegram, dataRaw]); - // Detect encryption by checking if the header is present. - if (this.detectEncryption && !this.hasStartOfFrame) { - const { isEncrypted, isAscii, requiresMoreData } = this.checkEncryption(); - - if (requiresMoreData) return; // Wait for more data to arrive. - - if (isEncrypted) { - const error = new DSMRDecryptionRequired(); - error.withRawTelegram(this.telegram); - this.options.callback(error, undefined); - this.telegram = Buffer.alloc(0); - return; - } - - if (!isAscii) { - const error = new DSMRDecodeError('Invalid frame (not in ascii range)'); - error.withRawTelegram(this.telegram); - this.options.callback(error, undefined); - this.telegram = Buffer.alloc(0); - return; - } - - // If we get here, the frame is not encrypted and is in ascii range. - // We can try parsing it as a normal DSMR frame. - } - if (!this.hasStartOfFrame) { const sofIndex = this.telegram.indexOf('/'); // Not yet a valid frame. Discard the data if (sofIndex === -1) { - const error = new DSMRStartOfFrameNotFoundError(); + const error = new StartOfFrameNotFoundError(); error.withRawTelegram(this.telegram); this.options.callback(error, undefined); this.telegram = Buffer.alloc(0); @@ -107,18 +71,11 @@ export class UnencryptedDSMRStreamParser implements DSMRStreamParser { // Check if the characters before the sof char are newlines. Otherwise the sof // can be part of a text message element of a telegram. - if (this.options.newLineChars === '\n' && sofIndex > 1) { - const bytesBeforeSof = this.telegram.subarray(sofIndex - 1, sofIndex); - - // 0x0a is a newline character. - if (bytesBeforeSof[0] !== 0x0a) { - return; - } - } else if (sofIndex > 2) { + if (sofIndex > 2) { const bytesBeforeSof = this.telegram.subarray(sofIndex - 2, sofIndex); - // 0x0d is a carriage return and 0x0a is a newline character. - if (bytesBeforeSof[0] !== 0x0d || bytesBeforeSof[1] !== 0x0a) { + // Check if the bytes before the start of frame are CRLF. + if (bytesBeforeSof[0] !== CR || bytesBeforeSof[1] !== LF) { return; } } @@ -134,48 +91,20 @@ export class UnencryptedDSMRStreamParser implements DSMRStreamParser { this.tryParseTelegram(endOfFrameIndex); } - private checkEncryption() { - const encryptedSof = this.telegram.indexOf(ENCRYPTED_DSMR_TELEGRAM_SOF); - - if (encryptedSof === -1) { - // There is no start of frame (for an encrypted frame) in the buffer. - return { - isEncrypted: false, - isAscii: isAsciiFrame(this.telegram), - }; - } - - // The header has a fixed length, so the telegram contain at least - // ENCRYPTED_DSMR_HEADER_LEN bytes after the start of frame. - const minimumTelegramLength = encryptedSof + ENCRYPTED_DSMR_HEADER_LEN; - - if (this.telegram.length < minimumTelegramLength) { - return { - requiresMoreData: true, - }; - } - - return { - isEncrypted: isEncryptedFrame(this.telegram), - isAscii: isAsciiFrame(this.telegram), - }; - } - private tryParseTelegram(frameLength: number, overrideError?: Error) { // Clear the full frame required timeout. The full frame // has been received and the data buffer will be cleared. clearTimeout(this.fullFrameRequiredTimeout); try { - const result = DSMRParser({ + const result = parseDsmr({ telegram: this.telegram.subarray(0, frameLength), - newLineChars: this.options.newLineChars, }); this.options.callback(null, result); } catch (err) { const error = overrideError ?? err; - if (error instanceof DSMRError) { + if (error instanceof SmartMeterError) { error.withRawTelegram(this.telegram); } @@ -193,7 +122,7 @@ export class UnencryptedDSMRStreamParser implements DSMRStreamParser { } private onFullFrameRequiredTimeout() { - this.tryParseTelegram(this.telegram.length, new DSMRTimeoutError()); + this.tryParseTelegram(this.telegram.length, new SmartMeterTimeoutError()); // Reset the entire state here, as the full frame was not received. this.clear(); diff --git a/src/stream/stream.ts b/src/stream/stream.ts new file mode 100644 index 0000000..70452a3 --- /dev/null +++ b/src/stream/stream.ts @@ -0,0 +1,16 @@ +import { SmartMeterParserResult } from '../index.js'; + +export type SmartMeterStreamParser = { + /** Stop the stream parser. */ + destroy(): void; + /** Clear all cached data */ + clear(): void; + /** Size in bytes of the data that is cached */ + currentSize(): number; + /** The byte that indicates a start of frame was found for this parser */ + readonly startOfFrameByte: number; +}; + +export type SmartMeterStreamCallback< + TResult extends SmartMeterParserResult = SmartMeterParserResult, +> = (error: unknown, result?: TResult) => void; diff --git a/src/util/base-result.ts b/src/util/base-result.ts new file mode 100644 index 0000000..6d56bca --- /dev/null +++ b/src/util/base-result.ts @@ -0,0 +1,94 @@ +export type BaseParserResult = { + cosem: { + knownObjects: string[]; + unknownObjects: string[]; + }; + metadata: { + dsmrVersion?: number; + timestamp?: string; // TODO make this a date object + equipmentId?: string; + events?: { + powerFailures?: number; + longPowerFailures?: number; + voltageSags?: { + l1?: number; + l2?: number; + l3?: number; + }; + voltageSwells?: { + l1?: number; + l2?: number; + l3?: number; + }; + }; + textMessage?: string; + numericMessage?: number; + }; + electricity: { + total?: { + received?: number; + returned?: number; + reactiveReturned?: number; + reactiveReceived?: number; + }; + tariffs?: Partial< + Record< + number, + { + received?: number; + returned?: number; + reactiveReturned?: number; + reactiveReceived?: number; + } + > + >; + currentTariff?: number; + voltage?: { + l1?: number; + l2?: number; + l3?: number; + }; + current?: { + l1?: number; + l2?: number; + l3?: number; + }; + powerReturnedTotal?: number; + powerReturned?: { + l1?: number; + l2?: number; + l3?: number; + }; + powerReceivedTotal?: number; + powerReceived?: { + l1?: number; + l2?: number; + l3?: number; + }; + reactivePowerReturnedTotal?: number; + reactivePowerReturned?: { + l1?: number; + l2?: number; + l3?: number; + }; + reactivePowerReceivedTotal?: number; + reactivePowerReceived?: { + l1?: number; + l2?: number; + l3?: number; + }; + }; + mBus: Record< + number, + { + deviceType?: number; + equipmentId?: string; + value?: number; + unit?: string; + timestamp?: string; // TODO: Parse to date object + recordingPeriodMinutes?: number; // DSMR + } + >; + /** Only set when encryption is used */ + additionalAuthenticatedDataValid?: boolean; +}; diff --git a/src/util/crc.ts b/src/util/crc.ts index e4e55b6..51d8d1e 100644 --- a/src/util/crc.ts +++ b/src/util/crc.ts @@ -1,49 +1,71 @@ -import { DEFAULT_FRAME_ENCODING } from './frame-validation.js'; +/** + * Mirrors the bits of a number. + * + * E.g. for 0b00001111_00001100 it returns 0b001100_11110000. + */ +const reflectBits = (x: number, numBits: number) => { + let r = 0; + for (let i = 0; i < numBits; i++) { + r = (r << 1) | (x & 1); + x >>= 1; + } + return r; +}; -/** Calculate the CRC16 value of a buffer. This will use the polynomial: x16+x15+x2+1 (IBM) */ -export const calculateCrc16 = (data: Buffer) => { - let crc = 0; +/** + * Creates a CRC16 function that can be used to calculate the CRC16 checksum for a given data + * buffer. + * + * @note This method only works for CRC16 checksum that have RefIn=RefOut=true. + */ +const makeReflectedCrc16Function = ({ + polynomial, + initial, + xorOut, +}: { + polynomial: number; + initial: number; + xorOut: number; +}) => { + const inversePolynomial = reflectBits(polynomial, 16); - for (const byte of data) { - crc ^= byte; + return (data: Buffer) => { + let crc = initial; - for (let i = 0; i < 8; i++) { - if ((crc & 0x0001) !== 0) { - // 0xA001 is the reversed polynomial used for this CRC. - crc = (crc >> 1) ^ 0xa001; - } else { - crc = crc >> 1; + for (const byte of data) { + crc ^= byte; + + for (let i = 0; i < 8; i++) { + if ((crc & 0x0001) !== 0) { + crc = (crc >> 1) ^ inversePolynomial; + } else { + crc = crc >> 1; + } } } - } - return crc; + return crc ^ xorOut; + }; }; /** - * CRC is a CRC16 value calculated over the preceding characters in the data message (from “/” to - * “!” using the polynomial: x16+x15+x2+1). CRC16 uses no XOR in, no XOR out and is computed with - * least significant bit first. The value is represented as 4 hexadecimal characters (MSB first). + * Calculates the CRC16 checksum using CRC-16/ARC. Used for DSMR. * - * @param telegram - * @param enteredCrc - * @returns + * {@link https://crccalc.com/?method=CRC-16/ARC} */ -export const isCrcValid = ({ - telegram, - crc, - newLineChars, -}: { - telegram: string; - crc: number; - newLineChars: '\r\n' | '\n'; -}) => { - // Strip the CRC from the telegram - const crcSplit = `${newLineChars}!`; - const telegramParts = telegram.split(crcSplit); - const strippedTelegram = telegramParts[0] + crcSplit; - - const calculatedCrc = calculateCrc16(Buffer.from(strippedTelegram, DEFAULT_FRAME_ENCODING)); +export const calculateCrc16Arc = makeReflectedCrc16Function({ + polynomial: 0x8005, + initial: 0x0000, + xorOut: 0x0000, +}); - return calculatedCrc === crc; -}; +/** + * Calculates the CRC16 checksum using CRC-16/IBM-SDLC. Used for HDLC. + * + * {@link https://crccalc.com/?method=CRC-16/IBM-SDLC} + */ +export const calculateCrc16IbmSdlc = makeReflectedCrc16Function({ + polynomial: 0x1021, + initial: 0xffff, + xorOut: 0xffff, +}); diff --git a/src/util/errors.ts b/src/util/errors.ts index 72d277a..885425d 100644 --- a/src/util/errors.ts +++ b/src/util/errors.ts @@ -1,4 +1,4 @@ -export class DSMRError extends Error { +export class SmartMeterError extends Error { rawTelegram?: Buffer; /** Optionally add the raw telegram that caused the error. */ @@ -7,17 +7,17 @@ export class DSMRError extends Error { } } -export class DSMRParserError extends DSMRError { +export class SmartMeterParserError extends SmartMeterError { constructor(message: string) { super(message); - this.name = 'DSMRParserError'; + this.name = 'SmartMeterParserError'; } } -export class DSMRDecryptionError extends DSMRError { +export class SmartMeterDecryptionError extends SmartMeterError { constructor(originalError: unknown) { - super('DSMR decryption failed: ', { cause: originalError }); - this.name = 'DSMRDecryptionError'; + super('Decryption failed: ', { cause: originalError }); + this.name = 'DecryptionError'; if (typeof originalError === 'string') { this.message += originalError; @@ -29,30 +29,37 @@ export class DSMRDecryptionError extends DSMRError { } } -export class DSMRDecodeError extends DSMRError { +export class SmartMeterDecodeError extends SmartMeterError { constructor(message: string) { super(message); - this.name = 'DSMRDecodeError'; + this.name = 'DecodeError'; } } -export class DSMRStartOfFrameNotFoundError extends DSMRDecodeError { +export class StartOfFrameNotFoundError extends SmartMeterDecodeError { constructor() { super('Start of frame not found'); - this.name = 'DSMRStartOfFrameNotFoundError'; + this.name = 'StartOfFrameNotFoundError'; } } -export class DSMRDecryptionRequired extends DSMRDecodeError { +export class SmartMeterDecryptionRequired extends SmartMeterDecodeError { constructor() { - super('Encrypted DSMR frame detected'); - this.name = 'DSMRDecryptionRequired'; + super('Encrypted frame detected'); + this.name = 'DecryptionRequired'; } } -export class DSMRTimeoutError extends DSMRDecodeError { +export class SmartMeterTimeoutError extends SmartMeterDecodeError { constructor() { super('Timeout while waiting for full frame'); - this.name = 'DSMRTimeoutError'; + this.name = 'TimeoutError'; + } +} + +export class SmartMeterUnknownMessageTypeError extends SmartMeterError { + constructor(message: string) { + super(message); + this.name = 'UnknownMessageTypeError'; } } diff --git a/src/util/frame-validation.ts b/src/util/frame-validation.ts deleted file mode 100644 index f4caa08..0000000 --- a/src/util/frame-validation.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { decodeHeader, ENCRYPTED_DSMR_TELEGRAM_SOF } from './encryption.js'; - -export const DEFAULT_FRAME_ENCODING = 'binary'; - -/** - * Check if a line contains only valid ascii characters. - * - * @note Need to disable `no-control-regex` rule because of the use of control characters. - */ -// eslint-disable-next-line no-control-regex -const ASCII_REGEX = /[^\x00-\x7F]/; - -/** - * Check if a line contains only valid ascii characters. If this is not the case, the line is either - * encrypted or contains invalid characters. - * - * @note Doing this check with a regex compared to a loop or using `find` is around three times faster. - */ -export const isAsciiFrame = (telegram: Buffer) => { - return !ASCII_REGEX.test(telegram.toString('binary')); -}; - -/** Check if the given frame is an encrypted frame. */ -export const isEncryptedFrame = (buffer: Buffer) => { - const sofIndex = buffer.indexOf(ENCRYPTED_DSMR_TELEGRAM_SOF); - - if (sofIndex === -1) return false; - - try { - const bufferAtHeader = buffer.subarray(sofIndex, buffer.length); - decodeHeader(bufferAtHeader); - return true; - } catch (_error) { - return false; - } -}; - -/** Check if received data is a valid frame, and if it is encrypted. */ -export const DSMRFrameValid = (telegram: Buffer) => { - const ascii = isAsciiFrame(telegram); - let encrypted = false; - - // Because sof of encrypted frame is 0xDB, the frame is not encrypted when it is valid ascii. - if (!ascii) { - encrypted = isEncryptedFrame(telegram); - } - - return { - valid: ascii || encrypted, - encrypted: encrypted, - }; -}; diff --git a/src/parsers/mbus.ts b/src/util/mbus.ts similarity index 82% rename from src/parsers/mbus.ts rename to src/util/mbus.ts index d9db668..bef79e7 100644 --- a/src/parsers/mbus.ts +++ b/src/util/mbus.ts @@ -1,4 +1,4 @@ -import type { DSMRParserResult } from '../index.js'; +import type { SmartMeterParserResult } from '../index.js'; export const MBUS_DEVICE_IDS = { gas: 0x03, @@ -8,7 +8,7 @@ export const MBUS_DEVICE_IDS = { export const getMbusDevice = ( deviceId: number | keyof typeof MBUS_DEVICE_IDS, - parsedData: DSMRParserResult, + parsedData: SmartMeterParserResult, ) => { const id = typeof deviceId === 'number' ? deviceId : MBUS_DEVICE_IDS[deviceId]; diff --git a/tests/crc.spec.ts b/tests/crc.spec.ts deleted file mode 100644 index 245547f..0000000 --- a/tests/crc.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, it } from 'node:test'; -import assert from 'node:assert'; -import { calculateCrc16, isCrcValid } from '../src/util/crc.js'; - -describe('CRC', () => { - describe('CRC16', () => { - // Note: these cases have been verified using https://crccalc.com/ - const CRC_TESTS = [ - { - input: 'Hello, world!', - output: 0x9a4a, - }, - { - input: '', - output: 0x0000, - }, - { - input: '123456789', - output: 0xbb3d, - }, - { - input: 'The quick brown fox jumps over the lazy dog', - output: 0xfcdf, - }, - { - input: 'Lorem ipsum dolor sit amet', - output: 0xc14f, - }, - ]; - - for (const test of CRC_TESTS) { - it(`Calculates the CRC of "${test.input}"`, () => { - const buf = Buffer.from(test.input); - const crc = calculateCrc16(buf); - assert.equal(crc, test.output); - }); - } - }); - - describe('Telegrams', () => { - it('Marks valid CRCs as valid', () => { - const invalid = '/TST512345\r\n\r\nHello, world!\r\n!25b5\r\n'; - const isValid = isCrcValid({ - telegram: invalid, - crc: 0x25b5, - newLineChars: '\r\n', - }); - assert.equal(isValid, true); - }); - - it('Marks invalid CRCs as invalid', () => { - const invalid = '/TST512345\r\n\r\nHello, world!\r\n!25b5\r\n'; - const isValid = isCrcValid({ - telegram: invalid, - crc: 0x25b5 + 1, - newLineChars: '\r\n', - }); - assert.equal(isValid, false); - }); - }); -}); diff --git a/tests/frame-validation.spec.ts b/tests/frame-validation.spec.ts deleted file mode 100644 index 87baf7c..0000000 --- a/tests/frame-validation.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import assert from 'node:assert'; -import { describe, it } from 'node:test'; -import { isAsciiFrame, isEncryptedFrame, DSMRFrameValid } from '../src/util/frame-validation.js'; -import { - encryptFrame, - getAllTestTelegramTestCases, - readTelegramFromFiles, - TEST_AAD, - TEST_DECRYPTION_KEY, -} from './test-utils.js'; - -describe('Frame validation', () => { - it('Detects ascii buffer', () => { - const buffer = Buffer.from('Hello, world!', 'utf-8'); - const isAscii = isAsciiFrame(buffer); - - assert.ok(isAscii); - }); - - it('Detects non-ascii buffer', () => { - const buffer = Buffer.from('Hello, 🌍!', 'utf-8'); - const isAscii = isAsciiFrame(buffer); - - assert.ok(!isAscii); - }); - - it('Detects invalid encrypted frame', () => { - const buffer = Buffer.from([0xdb, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); - const isEncrypted = isEncryptedFrame(buffer); - - assert.ok(!isEncrypted); - }); - - it('Detects valid encrypted frame', () => { - const buffer = encryptFrame({ - frame: 'Hello, world!', - key: TEST_DECRYPTION_KEY, - aad: TEST_AAD, - }); - const isEncrypted = isEncryptedFrame(buffer); - - assert.ok(isEncrypted); - }); - - it('Detects encrypted frame', () => { - const buffer = encryptFrame({ - frame: 'Hello, world!', - key: TEST_DECRYPTION_KEY, - aad: TEST_AAD, - }); - const { valid, encrypted } = DSMRFrameValid(buffer); - - assert.ok(valid); - assert.ok(encrypted); - }); - - describe('Detects ascii frames', async () => { - const cases = await getAllTestTelegramTestCases(); - - for (const testCase of cases) { - it(`Detects unencrypted frame in ${testCase}`, async () => { - const { input } = await readTelegramFromFiles(`./tests/telegrams/${testCase}`); - - const { valid, encrypted } = DSMRFrameValid(Buffer.from(input, 'utf-8')); - - assert.ok(valid); - assert.equal(encrypted, false); - }); - } - }); -}); diff --git a/tests/dsmr-parser.spec.ts b/tests/protocols/dsmr.spec.ts similarity index 60% rename from tests/dsmr-parser.spec.ts rename to tests/protocols/dsmr.spec.ts index a014ec3..72b3548 100644 --- a/tests/dsmr-parser.spec.ts +++ b/tests/protocols/dsmr.spec.ts @@ -1,40 +1,40 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { DSMR } from '../src/index.js'; +import { DSMR } from '../../src/index.js'; import { encryptFrame, - getAllTestTelegramTestCases, + getAllDSMRTestTelegramTestCases, readTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY, -} from './test-utils.js'; +} from './../test-utils.js'; +import { isDsmrCrcValid, parseDsmr } from '../../src/protocols/dsmr.js'; -describe('DSMR Parser', async () => { - const testCases = await getAllTestTelegramTestCases(); +describe('DSMR', async () => { + const testCases = await getAllDSMRTestTelegramTestCases(); for (const testCase of testCases) { it(`Parses ${testCase}`, async () => { const { input, output: expectedOutput } = await readTelegramFromFiles( - `./tests/telegrams/${testCase}`, + `./tests/telegrams/dsmr/${testCase}`, ); - const parsed = DSMR.parse({ - telegram: input, - }); + const parsed = parseDsmr({ telegram: input }); - // @ts-expect-error raw is not typed - assert.equal(parsed.raw, expectedOutput.raw); + // @ts-expect-error output is not typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + assert.equal(parsed.dsmr.raw, expectedOutput.dsmr.raw); assert.deepStrictEqual(JSON.parse(JSON.stringify(parsed)), expectedOutput); }); it(`Parses ${testCase} with decryption (valid AAD)`, async () => { const { input, output: expectedOutput } = await readTelegramFromFiles( - `./tests/telegrams/${testCase}`, + `./tests/telegrams/dsmr/${testCase}`, ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const parsed = DSMR.parse({ + const parsed = parseDsmr({ telegram: encrypted, decryptionKey: TEST_DECRYPTION_KEY, additionalAuthenticatedData: TEST_AAD, @@ -45,12 +45,12 @@ describe('DSMR Parser', async () => { it(`Parses ${testCase} with decryption (missing AAD)`, async () => { const { input, output: expectedOutput } = await readTelegramFromFiles( - `./tests/telegrams/${testCase}`, + `./tests/telegrams/dsmr/${testCase}`, ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const parsed = DSMR.parse({ + const parsed = parseDsmr({ telegram: encrypted, decryptionKey: TEST_DECRYPTION_KEY, additionalAuthenticatedData: undefined, @@ -61,12 +61,12 @@ describe('DSMR Parser', async () => { it(`Parses ${testCase} with decryption (invalid AAD)`, async () => { const { input, output: expectedOutput } = await readTelegramFromFiles( - `./tests/telegrams/${testCase}`, + `./tests/telegrams/dsmr/${testCase}`, ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const parsed = DSMR.parse({ + const parsed = parseDsmr({ telegram: encrypted, decryptionKey: TEST_DECRYPTION_KEY, additionalAuthenticatedData: Buffer.from('invalid-aad12345', 'ascii'), @@ -77,9 +77,9 @@ describe('DSMR Parser', async () => { } it('Gets m-bus data', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr-5.0-spec-example'); + const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); - const parsed = DSMR.parse({ telegram: input }); + const parsed = parseDsmr({ telegram: input }); const mbusData = DSMR.getMbusDevice('gas', parsed); @@ -91,28 +91,27 @@ describe('DSMR Parser', async () => { const input = "Hello, world! I'm not a valid telegram."; assert.throws(() => { - DSMR.parse({ telegram: input }); + parseDsmr({ telegram: input }); }); }); - it('Decodes using \\n characters', async () => { - // Note: use this file specifically because it doesn't have a CRC. The CRC is calculated using \r\n characters in - // the other files, thus the assert would fail. - const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-3.0-spec-example', - false, - ); - - // Need to manually replace \r\n with \n because the expected output is using \r\n - // @ts-expect-error output is not typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - output.raw = output.raw.replace(/\r\n/g, '\n'); - - const parsed = DSMR.parse({ - telegram: input, - newLineChars: '\n', + describe('CRC Validation', () => { + it('Marks valid CRCs as valid', () => { + const invalid = '/TST512345\r\n\r\nHello, world!\r\n!25b5\r\n'; + const isValid = isDsmrCrcValid({ + telegram: invalid, + crc: 0x25b5, + }); + assert.equal(isValid, true); }); - assert.deepStrictEqual(parsed, output); + it('Marks invalid CRCs as invalid', () => { + const invalid = '/TST512345\r\n\r\nHello, world!\r\n!25b5\r\n'; + const isValid = isDsmrCrcValid({ + telegram: invalid, + crc: 0x25b5 + 1, + }); + assert.equal(isValid, false); + }); }); }); diff --git a/tests/encryption.spec.ts b/tests/protocols/encryption.spec.ts similarity index 59% rename from tests/encryption.spec.ts rename to tests/protocols/encryption.spec.ts index f7f3c74..9e1a00e 100644 --- a/tests/encryption.spec.ts +++ b/tests/protocols/encryption.spec.ts @@ -1,30 +1,33 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; -import { readHexFile, readTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY } from './test-utils.js'; -import { decryptFrame } from '../src/util/encryption.js'; -import { DSMRDecryptionError } from '../src/index.js'; -import { DEFAULT_FRAME_ENCODING } from '../src/util/frame-validation.js'; -import { DSMRParser } from '../src/parsers/dsmr.js'; +import { + readHexFile, + readTelegramFromFiles, + TEST_AAD, + TEST_DECRYPTION_KEY, +} from './../test-utils.js'; +import { decryptDlmsFrame } from '../../src/protocols/encryption.js'; +import { SmartMeterDecryptionError } from '../../src/index.js'; +import { parseDsmr } from '../../src/protocols/dsmr.js'; describe('Encryption', async () => { const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-luxembourgh-spec-example', + './tests/telegrams/dsmr/dsmr-luxembourgh-spec-example', ); const encryptedWithAad = await readHexFile( - './tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt', + './tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt', ); const encryptedWithoutAad = await readHexFile( - './tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt', + './tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt', ); // This is not a real test, but at least it shows that the decryption works. // Ideally we add some real encrypted telegrams to the test suite. it('Can decrypt a message (with AAD)', async () => { - const decrypted = decryptFrame({ + const decrypted = decryptDlmsFrame({ data: encryptedWithAad, key: TEST_DECRYPTION_KEY, additionalAuthenticatedData: TEST_AAD, - encoding: DEFAULT_FRAME_ENCODING, }); assert.deepStrictEqual(decrypted.content.toString(), input); @@ -32,10 +35,9 @@ describe('Encryption', async () => { }); it('Can decrypt a message (without AAD)', async () => { - const decrypted = decryptFrame({ + const decrypted = decryptDlmsFrame({ data: encryptedWithoutAad, key: TEST_DECRYPTION_KEY, - encoding: DEFAULT_FRAME_ENCODING, }); assert.deepStrictEqual(decrypted.content.toString(), input); @@ -43,51 +45,47 @@ describe('Encryption', async () => { }); it('Can decrypt a message (with invalid AAD)', async () => { - const decrypted = decryptFrame({ + const decrypted = decryptDlmsFrame({ data: encryptedWithAad, key: TEST_DECRYPTION_KEY, additionalAuthenticatedData: Buffer.from('invalid-aad12345', 'ascii'), - encoding: DEFAULT_FRAME_ENCODING, }); assert.deepStrictEqual(decrypted.content.toString(), input); - assert.equal(decrypted.error?.constructor, DSMRDecryptionError); + assert.equal(decrypted.error?.constructor, SmartMeterDecryptionError); }); it('Returns error on invalid key', () => { - const { error } = decryptFrame({ + const { error } = decryptDlmsFrame({ data: encryptedWithAad, key: Buffer.from('invalid-key12345', 'ascii'), additionalAuthenticatedData: TEST_AAD, - encoding: DEFAULT_FRAME_ENCODING, }); - assert.equal(error?.constructor, DSMRDecryptionError); + assert.equal(error?.constructor, SmartMeterDecryptionError); }); it('Returns error on invalid AAD', () => { - const { content, error } = decryptFrame({ + const { content, error } = decryptDlmsFrame({ data: encryptedWithAad, key: TEST_DECRYPTION_KEY, additionalAuthenticatedData: Buffer.from('invalid-aad12345', 'ascii'), - encoding: DEFAULT_FRAME_ENCODING, }); - assert.equal(error?.constructor, DSMRDecryptionError); + assert.equal(error?.constructor, SmartMeterDecryptionError); - const parsed = DSMRParser({ telegram: content }); + const parsed = parseDsmr({ telegram: content }); assert.deepStrictEqual(JSON.parse(JSON.stringify(parsed)), output); }); it('Returns error on invalid key and AAD', () => { - const { error } = decryptFrame({ + const { error } = decryptDlmsFrame({ data: encryptedWithAad, key: Buffer.from('invalid-key12345', 'ascii'), additionalAuthenticatedData: Buffer.from('invalid-aad12345', 'ascii'), - encoding: DEFAULT_FRAME_ENCODING, }); - assert.equal(error?.constructor, DSMRDecryptionError); + assert.equal(error?.constructor, SmartMeterDecryptionError); }); }); diff --git a/tests/protocols/obis-code.spec.ts b/tests/protocols/obis-code.spec.ts new file mode 100644 index 0000000..eb7d2e0 --- /dev/null +++ b/tests/protocols/obis-code.spec.ts @@ -0,0 +1,230 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; + +import { + isEqualObisCode, + ObisCodeString, + parseObisCodeFromBuffer, + parseObisCodeFromString, + parseObisCodeWithWildcards, +} from '../../src/protocols/obis-code.js'; + +describe('OBIS Code', () => { + describe('parseObisCodeFromString', () => { + it('Parses a valid OBIS code', () => { + const str = '1-2:3.4.5'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.deepEqual(obisCode, { + media: 1, + channel: 2, + physical: 3, + type: 4, + processing: 5, + history: 0xff, + }); + assert.equal(consumedChars, str.length); + }); + + it('Parses a valid OBIS code in a longer string', () => { + const str = '1-2:3.4.5 some other text'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.deepEqual(obisCode, { + media: 1, + channel: 2, + physical: 3, + type: 4, + processing: 5, + history: 0xff, + }); + assert.equal(consumedChars, 9); + }); + + it('Parses OBIS code with large numbers', () => { + const str = '999-888:777.666.555'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.deepEqual(obisCode, { + media: 999, + channel: 888, + physical: 777, + type: 666, + processing: 555, + history: 0xff, + }); + assert.equal(consumedChars, str.length); + }); + + it('Returns null for invalid OBIS code', () => { + const str = 'invalid-obis-code'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for empty string', () => { + const str = ''; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for string with only delimiters', () => { + const str = '-:..'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for too large numbers', () => { + const str = '1000-1000:1000.1000.1000'; + const { obisCode, consumedChars } = parseObisCodeFromString(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + }); + + describe('parseObisCodeFromBuffer', () => { + it('Parses a valid OBIS code', () => { + const buf = Buffer.from('010203040506', 'hex'); + const { obisCode } = parseObisCodeFromBuffer(buf); + + assert.deepEqual(obisCode, { + media: 1, + channel: 2, + physical: 3, + type: 4, + processing: 5, + history: 6, + }); + }); + + it('Returns null when buffer is longer than obis code', () => { + const buf = Buffer.from('010203040506aabbccddeeff', 'hex'); + const { obisCode } = parseObisCodeFromBuffer(buf); + assert.deepEqual(obisCode, null); + }); + + it('Returns null when buffer is shorter than obis code', () => { + const buf = Buffer.from('0102', 'hex'); + const { obisCode } = parseObisCodeFromBuffer(buf); + assert.deepEqual(obisCode, null); + }); + }); + + describe('parseObisCodeWithWildcards', () => { + it('Parses a valid OBIS code', () => { + const str = '1-*:3.4.5'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.deepEqual(obisCode, { + media: 1, + channel: '*', + physical: 3, + type: 4, + processing: 5, + history: '*', + }); + assert.equal(consumedChars, str.length); + }); + + it('Parses a valid OBIS code in a longer string', () => { + const str = '1-2:3.4.* some other text'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.deepEqual(obisCode, { + media: 1, + channel: 2, + physical: 3, + type: 4, + processing: '*', + history: '*', + }); + assert.equal(consumedChars, 9); + }); + + it('Parses OBIS code with large numbers', () => { + const str = '999-888:*.666.555'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.deepEqual(obisCode, { + media: 999, + channel: 888, + physical: '*', + type: 666, + processing: 555, + history: '*', + }); + assert.equal(consumedChars, str.length); + }); + + it('Returns null for invalid OBIS code', () => { + const str = 'invalid-obis-code'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for empty string', () => { + const str = ''; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for string with only delimiters', () => { + const str = '-:..'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + + it('Returns null for too large numbers', () => { + const str = '1000-1000:1000.1000.1000'; + const { obisCode, consumedChars } = parseObisCodeWithWildcards(str); + + assert.equal(obisCode, null); + assert.equal(consumedChars, 0); + }); + }); + + describe('isEqualObisCode', () => { + const testIsEqualObisCode = ( + codeA: ObisCodeString, + codeB: ObisCodeString, + expected: boolean, + ) => { + it(`${codeA} ${expected ? '===' : '!=='} ${codeB}`, () => { + const { obisCode: obisCodeA } = parseObisCodeWithWildcards(codeA); + const { obisCode: obisCodeB } = parseObisCodeWithWildcards(codeB); + + assert.ok(obisCodeA !== null); + assert.ok(obisCodeB !== null); + + const result = isEqualObisCode(obisCodeA, obisCodeB); + assert.equal(result, expected, `Expected ${codeA} ${expected ? '===' : '!=='} ${codeB}`); + }); + }; + + testIsEqualObisCode('1-2:3.4.5', '1-2:3.4.5', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:3.4.*', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:3.*.*', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:*.4.5', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:*.4.*', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:*.4.*', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:*.4.*', true); + testIsEqualObisCode('1-2:3.4.5', '1-2:3.*.*', true); + testIsEqualObisCode('*-2:3.4.5', '1-2:3.*.5', true); + + testIsEqualObisCode('1-2:3.4.5', '5-4:3.2.1', false); + testIsEqualObisCode('1-2:3.4.5', '1-2:*.4.6', false); + }); +}); diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts new file mode 100644 index 0000000..2741d6f --- /dev/null +++ b/tests/stream/stream-detect-type.spec.ts @@ -0,0 +1,239 @@ +import assert from 'node:assert'; +import { PassThrough } from 'node:stream'; +import { describe, it, mock } from 'node:test'; +import { chunkBuffer, readHexFile, readTelegramFromFiles } from './../test-utils.js'; +import { StreamDetectType } from '../../src/stream/stream-detect-type.js'; + +describe('Stream: Detect Type', () => { + it('Detects unencrypted DSMR telegrams', async () => { + const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], { + mode: 'dsmr', + encrypted: false, + data: Buffer.from(input), + }); + }); + + it('Detects unencrypted DSMR telegrams (chunks)', async () => { + const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const chunks = chunkBuffer(Buffer.from(input), 1); + const detector = new StreamDetectType({ stream, callback }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + + const arg0 = callback.mock.calls[0].arguments[0] as { + mode: string; + encrypted: boolean; + data: Buffer; + }; + assert.equal(arg0.mode, 'dsmr'); + assert.equal(arg0.encrypted, false); + assert.ok(Buffer.isBuffer(arg0.data)); + }); + + it('Detects encrypted DSMR telegrams', async () => { + const input = await readHexFile( + './tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt', + ); + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], { + mode: 'dsmr', + encrypted: true, + data: input, + }); + }); + + it('Detects encrypted DSMR telegrams (chunks)', async () => { + const input = await readHexFile( + './tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt', + ); + const stream = new PassThrough(); + const callback = mock.fn(); + + const chunks = chunkBuffer(input, 1); + const detector = new StreamDetectType({ stream, callback }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + + const arg0 = callback.mock.calls[0].arguments[0] as { + mode: string; + encrypted: boolean; + data: Buffer; + }; + assert.equal(arg0.mode, 'dsmr'); + assert.equal(arg0.encrypted, true); + assert.ok(Buffer.isBuffer(arg0.data)); + }); + + it('Detects unencrypted DLMS telegrams', async () => { + const input = await readHexFile('./tests/telegrams/dlms/aidon-example-1.txt'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], { + mode: 'dlms', + encrypted: false, + data: input, + }); + }); + + it('Detects unencrypted DLMS telegrams (chunks)', async () => { + const input = await readHexFile('./tests/telegrams/dlms/aidon-example-1.txt'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const chunks = chunkBuffer(input, 1); + const detector = new StreamDetectType({ stream, callback }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + + const arg0 = callback.mock.calls[0].arguments[0] as { + mode: string; + encrypted: boolean; + data: Buffer; + }; + assert.equal(arg0.mode, 'dlms'); + assert.equal(arg0.encrypted, false); + assert.ok(Buffer.isBuffer(arg0.data)); + }); + + it('Detects encrypted DLMS telegrams', async () => { + const input = await readHexFile('./tests/telegrams/dlms/radiusel-example.txt'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], { + mode: 'dlms', + encrypted: true, + data: input, + }); + }); + + it('Detects encrypted DLMS telegrams (chunks)', async () => { + const input = await readHexFile('./tests/telegrams/dlms/radiusel-example.txt'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const chunks = chunkBuffer(input, 1); + const detector = new StreamDetectType({ stream, callback }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + + const arg0 = callback.mock.calls[0].arguments[0] as { + mode: string; + encrypted: boolean; + data: Buffer; + }; + assert.equal(arg0.mode, 'dlms'); + assert.equal(arg0.encrypted, true); + assert.ok(Buffer.isBuffer(arg0.data)); + }); + + it('Clears random data', async () => { + const input = Buffer.from('this is not a telegram'); + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + + assert.equal(detector.currentSize(), 0); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 0); + }); + + describe('Handles invalid HDLC headers', () => { + const test = (input: Buffer, expectedCalls: number) => { + it(input.toString('hex'), async () => { + const stream = new PassThrough(); + const callback = mock.fn(); + + const detector = new StreamDetectType({ stream, callback }); + + stream.write(input); + stream.end(); + detector.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, expectedCalls); + }); + }; + + // This is a valid HDLC header + test(Buffer.from('7EA0E22B2113239AE6E700000000', 'hex'), 1); + + // These are not (note that some bytes can be 0x00, and still result in a valid header) + test(Buffer.from('00A0E22B2113239AE6E700000000', 'hex'), 0); + test(Buffer.from('7E00E22B2113239AE6E700000000', 'hex'), 0); + test(Buffer.from('7EA0E2002113239AE6E700000000', 'hex'), 0); + test(Buffer.from('7EA0E22B0013239AE6E700000000', 'hex'), 0); + test(Buffer.from('7EA0E22B2113239A00E700000000', 'hex'), 0); + test(Buffer.from('7EA0E22B2113239AE60000000000', 'hex'), 0); + }); +}); diff --git a/tests/stream.spec.ts b/tests/stream/stream-dsmr.spec.ts similarity index 61% rename from tests/stream.spec.ts rename to tests/stream/stream-dsmr.spec.ts index df6e664..ac6932a 100644 --- a/tests/stream.spec.ts +++ b/tests/stream/stream-dsmr.spec.ts @@ -8,18 +8,19 @@ import { readTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY, -} from './test-utils.js'; +} from './../test-utils.js'; import { - DSMRStartOfFrameNotFoundError, - DSMR, - DSMRDecryptionRequired, - DSMRDecodeError, - DSMRTimeoutError, - DSMRDecryptionError, - DSMRParserResult, - DSMRParserError, -} from '../src/index.js'; -import { ENCRYPTED_DSMR_HEADER_LEN, ENCRYPTED_DSMR_TELEGRAM_SOF } from '../src/util/encryption.js'; + StartOfFrameNotFoundError, + SmartMeterTimeoutError, + SmartMeterDecryptionError, + SmartMeterParserResult, + UnencryptedDSMRStreamParser, + EncryptedDSMRStreamParser, +} from '../../src/index.js'; +import { + ENCRYPTED_DLMS_HEADER_LEN, + ENCRYPTED_DLMS_TELEGRAM_SOF, +} from '../../src/protocols/encryption.js'; const assertDecryptedFrameValid = ({ actual, @@ -30,7 +31,7 @@ const assertDecryptedFrameValid = ({ expected: object; aadValid: boolean; }) => { - const parsed = actual as DSMRParserResult; + const parsed = actual as SmartMeterParserResult; assert.equal(parsed.additionalAuthenticatedDataValid, aadValid); // Note: this field is not in the output, because the output was not created with encryption enabled. @@ -44,7 +45,7 @@ describe('DSMRStreamParser', () => { describe('Unencrypted', () => { it('Parses a chunked unencrypted telegram', async () => { const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const chunks = chunkString(input, 10); @@ -52,7 +53,7 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ stream, callback }); + const instance = new UnencryptedDSMRStreamParser({ stream, callback }); for (const chunk of chunks) { stream.write(chunk); @@ -67,16 +68,16 @@ describe('DSMRStreamParser', () => { it('Parses two unencrypted telegrams', async () => { const { input: input1, output: output1 } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const { input: input2, output: output2 } = await readTelegramFromFiles( - './tests/telegrams/dsmr-4.0-spec-example', + './tests/telegrams/dsmr/dsmr-4.0-spec-example', ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ stream, callback }); + const instance = new UnencryptedDSMRStreamParser({ stream, callback }); stream.write(input1 + input2); @@ -94,74 +95,44 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ stream, callback }); + const instance = new UnencryptedDSMRStreamParser({ stream, callback }); const data = 'invalid telegram xxx yyy'; // Make sure the telegram is at least ENCRYPTED_DSMR_HEADER_LEN long to // allow encrypted frames to be detected. - assert.ok(data.length >= ENCRYPTED_DSMR_HEADER_LEN); + assert.ok(data.length >= ENCRYPTED_DLMS_HEADER_LEN); stream.write(data); stream.end(); instance.destroy(); assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRStartOfFrameNotFoundError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(callback.mock.calls[0].arguments[1], undefined); }); - it('Parses a telegram with a different newline character', async () => { - // Note: use this file specifically because it doesn't have a CRC. The CRC is calculated using \r\n characters in - // the other files, thus the assert would fail. - const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-3.0-spec-example', - false, - ); - - // Need to manually replace \r\n with \n because the expected output is using \r\n - // @ts-expect-error output is not typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - output.raw = output.raw.replace(/\r\n/g, '\n'); - - const stream = new PassThrough(); - const callback = mock.fn(); - - const instance = DSMR.createStreamParser({ - stream, - callback, - newLineChars: '\n', - }); - stream.write(input); - - stream.end(); - instance.destroy(); - - assert.deepStrictEqual(callback.mock.calls.length, 1); - assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); - }); - it("Doesn't throw error after receiving null character", async () => { // Note: some meters send a null character (\0) at the end of the telegram. This should be ignored. const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ stream, callback }); + const instance = new UnencryptedDSMRStreamParser({ stream, callback }); stream.write(input + '\0'); stream.end(); instance.destroy(); assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], { - ...output, - // @ts-expect-error output is not typed - raw: output.raw + '\0', - }); + + // Need to manually add \0 to the output + // @ts-expect-error output is not typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + output.dsmr.raw += '\0'; + assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); }); it('Throws an error if a full frame is not received in time', async (context) => { @@ -172,11 +143,10 @@ describe('DSMRStreamParser', () => { const fullFrameRequiredWithinMs = 5000; - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs, - detectEncryption: false, }); stream.write('/'); // Start by writing the start of the telegram @@ -193,7 +163,7 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRTimeoutError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); assert.equal(instance.currentSize(), 0); // Writing more data should trigger the sof error again. @@ -202,7 +172,7 @@ describe('DSMRStreamParser', () => { context.mock.timers.tick(fullFrameRequiredWithinMs); assert.equal(callback.mock.calls.length, 2); - assert.ok(callback.mock.calls[1].arguments[0] instanceof DSMRStartOfFrameNotFoundError); + assert.ok(callback.mock.calls[1].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -216,11 +186,10 @@ describe('DSMRStreamParser', () => { const fullFrameRequiredWithinMs = 5000; - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs, - detectEncryption: false, }); stream.write('/'); // Start by writing the start of the telegram @@ -229,7 +198,7 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRTimeoutError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -238,12 +207,14 @@ describe('DSMRStreamParser', () => { it('Parses when the CRC line is missing', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles('tests/telegrams/iskra-mt-382-no-crc'); + const { input, output } = await readTelegramFromFiles( + 'tests/telegrams/dsmr/iskra-mt-382-no-crc', + ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs: 1000, @@ -265,14 +236,14 @@ describe('DSMRStreamParser', () => { context.mock.timers.enable(); const { input, output } = await readTelegramFromFiles( - 'tests/telegrams/iskra-mt-382-no-crc', + 'tests/telegrams/dsmr/iskra-mt-382-no-crc', true, ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs: 1000, @@ -298,12 +269,14 @@ describe('DSMRStreamParser', () => { it('Immediately parses when CRC is missing and a three telegrams are received', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles('tests/telegrams/iskra-mt-382-no-crc'); + const { input, output } = await readTelegramFromFiles( + 'tests/telegrams/dsmr/iskra-mt-382-no-crc', + ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs: 1000, @@ -333,13 +306,13 @@ describe('DSMRStreamParser', () => { context.mock.timers.enable(); const { input, output } = await readTelegramFromFiles( - 'tests/telegrams/iskra-mt-382-no-crc-with-text-message', + 'tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message', ); const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new UnencryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs: 1000, @@ -361,7 +334,7 @@ describe('DSMRStreamParser', () => { describe('Encrypted', () => { it('Parses a chunked encrypted telegram', async () => { const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); const chunks = chunkBuffer(encrypted, 10); @@ -369,7 +342,7 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: TEST_DECRYPTION_KEY, @@ -393,10 +366,10 @@ describe('DSMRStreamParser', () => { it('Parses two encrypted telegrams', async () => { const { input: input1, output: output1 } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const { input: input2, output: output2 } = await readTelegramFromFiles( - './tests/telegrams/dsmr-4.0-spec-example', + './tests/telegrams/dsmr/dsmr-4.0-spec-example', ); const encrypted1 = encryptFrame({ frame: input1, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); @@ -405,7 +378,7 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: TEST_DECRYPTION_KEY, @@ -436,7 +409,7 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: TEST_DECRYPTION_KEY, @@ -449,144 +422,10 @@ describe('DSMRStreamParser', () => { instance.destroy(); assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRStartOfFrameNotFoundError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(callback.mock.calls[0].arguments[1], undefined); }); - it('Parses a telegram with a different newline character', async () => { - // Note: use this file specifically because it doesn't have a CRC. The CRC is calculated using \r\n characters in - // the other files, thus the assert would fail. - const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-3.0-spec-example', - false, - ); - - // Need to manually replace \r\n with \n because the expected output is using \r\n - // @ts-expect-error output is not typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - output.raw = output.raw.replace(/\r\n/g, '\n'); - - const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - - const stream = new PassThrough(); - const callback = mock.fn(); - - const instance = DSMR.createStreamParser({ - stream, - callback, - newLineChars: '\n', - decryptionKey: TEST_DECRYPTION_KEY, - additionalAuthenticatedData: TEST_AAD, - }); - stream.write(encrypted); - - stream.end(); - instance.destroy(); - - assert.deepStrictEqual(callback.mock.calls.length, 1); - assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assertDecryptedFrameValid({ - actual: callback.mock.calls[0].arguments[1], - expected: output, - aadValid: true, - }); - }); - - it('Detects an encrypted frame in non-encrypted mode', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr-5.0-spec-example'); - const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const chunks = chunkBuffer(encrypted, 1); - - const stream = new PassThrough(); - const callback = mock.fn(); - - const instance = DSMR.createStreamParser({ - stream, - callback, - detectEncryption: true, - additionalAuthenticatedData: TEST_AAD, - }); - - for (const chunk of chunks) { - stream.write(chunk); - } - - stream.end(); - instance.destroy(); - - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRDecryptionRequired); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], undefined); - - // If the encrypted data contains a start of frame in the final chunks, there could be remaining - // data left in the buffer, because it is waiting until it has enough data to detect the header of - // the encrypted frame. - assert.ok(instance.currentSize() < 2 * ENCRYPTED_DSMR_HEADER_LEN); - - // Because everything is coming in as small chunks, it will be calling the callback multiple times. - // Each time it should be a DSMRStartOfFrameNotFoundError error, because only after the first chunks - // it should be able to detect that it is an encrypted frame. - for (let index = 1; index < callback.mock.calls.length; index++) { - const error = callback.mock.calls[index].arguments[0] as unknown; - assert.ok( - (error instanceof DSMRDecodeError && !(error instanceof DSMRDecryptionRequired)) || - error instanceof DSMRParserError, - ); - assert.deepStrictEqual(callback.mock.calls[index].arguments[1], undefined); - } - }); - - // Make sure that if the first chunk does not contain the - // full header, it will can still detect the encrypted frame when - it('Detects non-aligned encrypted frame', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr-5.0-spec-example'); - const originalEncrypted = encryptFrame({ - frame: input, - key: TEST_DECRYPTION_KEY, - aad: TEST_AAD, - }); - - const prefix = Buffer.from( - [...new Array(ENCRYPTED_DSMR_HEADER_LEN - 1)].map(() => 0x00), - ); - - const encrypted = Buffer.concat([prefix, originalEncrypted]); - const chunks = chunkBuffer(encrypted, ENCRYPTED_DSMR_HEADER_LEN); - - const stream = new PassThrough(); - const callback = mock.fn(); - - const instance = DSMR.createStreamParser({ - stream, - callback, - detectEncryption: true, - additionalAuthenticatedData: TEST_AAD, - }); - - for (const chunk of chunks) { - stream.write(chunk); - } - - stream.end(); - instance.destroy(); - - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRDecryptionRequired); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], undefined); - - // If the encrypted data contains a start of frame in the final chunks, there could be remaining - // data left in the buffer, because it is waiting until it has enough data to detect the header of - // the encrypted frame. - assert.ok(instance.currentSize() < 2 * ENCRYPTED_DSMR_HEADER_LEN); - - // Because everything is coming in as small chunks, it will be calling the callback multiple times. - // Each time it should be a kind of DSMRDecodeError error, because only after the first chunks - // it should be able to detect that it is an encrypted frame. - for (let index = 1; index < callback.mock.calls.length; index++) { - const error = callback.mock.calls[index].arguments[0] as unknown; - assert.ok(error instanceof DSMRDecodeError && !(error instanceof DSMRDecryptionRequired)); - assert.deepStrictEqual(callback.mock.calls[index].arguments[1], undefined); - } - }); - it('Throws an error if a full frame is not received in time', async (context) => { context.mock.timers.enable(); @@ -595,7 +434,7 @@ describe('DSMRStreamParser', () => { const fullFrameRequiredWithinMs = 5000; - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs, @@ -604,7 +443,7 @@ describe('DSMRStreamParser', () => { }); const frame = encryptFrame({ frame: '', key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const header = frame.subarray(0, ENCRYPTED_DSMR_HEADER_LEN); + const header = frame.subarray(0, ENCRYPTED_DLMS_HEADER_LEN); stream.write(header); // Write the header, but not the rest of the frame. @@ -612,7 +451,7 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRTimeoutError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -626,22 +465,21 @@ describe('DSMRStreamParser', () => { const fullFrameRequiredWithinMs = 5000; - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, fullFrameRequiredWithinMs, decryptionKey: TEST_DECRYPTION_KEY, additionalAuthenticatedData: TEST_AAD, - detectEncryption: false, }); - stream.write(Buffer.from([ENCRYPTED_DSMR_TELEGRAM_SOF])); // Start by writing the start of the telegram + stream.write(Buffer.from([ENCRYPTED_DLMS_TELEGRAM_SOF])); // Start by writing the start of the telegram context.mock.timers.tick(fullFrameRequiredWithinMs); // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRTimeoutError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -650,10 +488,10 @@ describe('DSMRStreamParser', () => { it('Throws an error if key is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr-5.0-spec-example'); + const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: Buffer.from('invalid-key12345', 'ascii'), @@ -666,7 +504,7 @@ describe('DSMRStreamParser', () => { instance.destroy(); assert.equal(callback.mock.calls.length, 1); - assert.ok(callback.mock.calls[0].arguments[0] instanceof DSMRDecryptionError); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterDecryptionError); assert.equal(callback.mock.calls[0].arguments[1], undefined); }); @@ -674,11 +512,11 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: TEST_DECRYPTION_KEY, @@ -704,11 +542,11 @@ describe('DSMRStreamParser', () => { const stream = new PassThrough(); const callback = mock.fn(); const { input, output } = await readTelegramFromFiles( - './tests/telegrams/dsmr-5.0-spec-example', + './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); - const instance = DSMR.createStreamParser({ + const instance = new EncryptedDSMRStreamParser({ stream, callback, decryptionKey: TEST_DECRYPTION_KEY, diff --git a/tests/telegrams/dlms/aidon-example-1.json b/tests/telegrams/dlms/aidon-example-1.json new file mode 100644 index 0000000..4953395 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-1.json @@ -0,0 +1,56 @@ +[ + { + "hdlc": { + "raw": "7ea0d24108831382d6e6e7000f40000000000109020209060101000281ff0a0b4149444f4e5f5630303031020209060000600100ff0a1037333539393932383930393431373432020209060000600107ff0a0436353135020309060100010700ff060000055202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000003e402020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10005d02020fff1621020309060100200700ff1209c402020fff1623e0c47e", + "header": { + "destinationAddress": 32, + "sourceAddress": 577, + "crc": { + "value": 54914, + "valid": true + } + }, + "crc": { + "value": 50400, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicStructure" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.2.129(AIDON_V0001)", + "0-0:96.1.7(6515)", + "1-0:3.7.0(996*var)", + "1-0:4.7.0(0*var)" + ], + "knownObjects": [ + "0-0:96.1.0(7359992890941742)", + "1-0:1.7.0(1362*W)", + "1-0:2.7.0(0*W)", + "1-0:31.7.0(9.3*A)", + "1-0:32.7.0(250*V)" + ] + }, + "electricity": { + "powerReceivedTotal": 1362000, + "powerReturnedTotal": 0, + "current": { + "l1": 9.3 + }, + "voltage": { + "l1": 250 + } + }, + "mBus": { + "0": { + "equipmentId": "7359992890941742" + } + }, + "metadata": {} + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-1.txt b/tests/telegrams/dlms/aidon-example-1.txt new file mode 100644 index 0000000..0077d64 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-1.txt @@ -0,0 +1,13 @@ +7e a0d2 41 0883 13 82d6 e6e700 + 0f 40000000 00 + 0109 + 0202 0906 0101000281ff 0a0b 4149444f4e5f5630303031 + 0202 0906 0000600100ff 0a10 37333539393932383930393431373432 + 0202 0906 0000600107ff 0a04 36353135 + 0203 0906 0100010700ff 06 00000552 0202 0f00 161b + 0203 0906 0100020700ff 06 00000000 0202 0f00 161b + 0203 0906 0100030700ff 06 000003e4 0202 0f00 161d + 0203 0906 0100040700ff 06 00000000 0202 0f00 161d + 0203 0906 01001f0700ff 10 005d 0202 0fff 1621 + 0203 0906 0100200700ff 12 09c4 0202 0fff 1623 +e0c4 7e \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2.json b/tests/telegrams/dlms/aidon-example-2.json new file mode 100644 index 0000000..fe085c5 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2.json @@ -0,0 +1,90 @@ +[ + { + "hdlc": { + "raw": "7ea2434108831385ebe6e7000f4000000000011b020209060000010000ff090c07e30c1001073b28ff8000ff020309060100010700ff060000046202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000005e302020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10000002020fff1621020309060100330700ff10004b02020fff1621020309060100470700ff10000002020fff1621020309060100200700ff12090302020fff1623020309060100340700ff1209c302020fff1623020309060100480700ff12090402020fff1623020309060100150700ff060000000002020f00161b020309060100160700ff060000000002020f00161b020309060100170700ff060000000002020f00161d020309060100180700ff060000000002020f00161d020309060100290700ff060000046202020f00161b0203090601002a0700ff060000000002020f00161b0203090601002b0700ff06000005e202020f00161d0203090601002c0700ff060000000002020f00161d0203090601003d0700ff060000000002020f00161b0203090601003e0700ff060000000002020f00161b0203090601003f0700ff060000000002020f00161d020309060100400700ff060000000002020f00161d020309060100010800ff060099598602020f00161e020309060100020800ff060000000802020f00161e020309060100030800ff060064ed4b02020f001620020309060100040800ff060000000502020f001620be407e", + "header": { + "destinationAddress": 32, + "sourceAddress": 577, + "crc": { + "value": 60293, + "valid": true + } + }, + "crc": { + "value": 16574, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicStructure" + }, + "cosem": { + "unknownObjects": [ + "1-0:3.7.0(1507*var)", + "1-0:4.7.0(0*var)", + "1-0:23.7.0(0*var)", + "1-0:24.7.0(0*var)", + "1-0:43.7.0(1506*var)", + "1-0:44.7.0(0*var)", + "1-0:63.7.0(0*var)", + "1-0:64.7.0(0*var)", + "1-0:3.8.0(6614347*varh)", + "1-0:4.8.0(5*varh)" + ], + "knownObjects": [ + "0-0:1.0.0(07e30c1001073b28ff8000ff)", + "1-0:1.7.0(1122*W)", + "1-0:2.7.0(0*W)", + "1-0:31.7.0(0*A)", + "1-0:51.7.0(7.5*A)", + "1-0:71.7.0(0*A)", + "1-0:32.7.0(230.70000000000002*V)", + "1-0:52.7.0(249.9*V)", + "1-0:72.7.0(230.8*V)", + "1-0:21.7.0(0*W)", + "1-0:22.7.0(0*W)", + "1-0:41.7.0(1122*W)", + "1-0:42.7.0(0*W)", + "1-0:61.7.0(0*W)", + "1-0:62.7.0(0*W)", + "1-0:1.8.0(10049926*Wh)", + "1-0:2.8.0(8*Wh)" + ] + }, + "electricity": { + "powerReceivedTotal": 1122000, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 7.5, + "l3": 0 + }, + "voltage": { + "l1": 230.70000000000002, + "l2": 249.9, + "l3": 230.8 + }, + "powerReceived": { + "l1": 0, + "l2": 1122, + "l3": 0 + }, + "powerReturned": { + "l1": 0, + "l2": 0, + "l3": 0 + }, + "total": { + "received": 10049926, + "returned": 8 + } + }, + "mBus": {}, + "metadata": { + "timestamp": "07e30c1001073b28ff8000ff" + } + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2.txt b/tests/telegrams/dlms/aidon-example-2.txt new file mode 100644 index 0000000..2f11931 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2.txt @@ -0,0 +1,31 @@ +7e a243 41 0883 13 85eb e6e700 + 0f 40000000 00 + 011b + 0202 0906 0000010000ff 090c 07e30c1001073b28ff8000ff + 0203 0906 0100010700ff 06 00000462 0202 0f00 161b + 0203 0906 0100020700ff 06 00000000 0202 0f00 161b + 0203 0906 0100030700ff 06 000005e3 0202 0f00 161d + 0203 0906 0100040700ff 06 00000000 0202 0f00 161d + 0203 0906 01001f0700ff 10 00000202 0fff 1621 + 0203 0906 0100330700ff 10 004b0202 0fff 1621 + 0203 0906 0100470700ff 10 00000202 0fff 1621 + 0203 0906 0100200700ff 12 09030202 0fff 1623 + 0203 0906 0100340700ff 12 09c30202 0fff 1623 + 0203 0906 0100480700ff 12 09040202 0fff 1623 + 0203 0906 0100150700ff 06 00000000 0202 0f00 161b + 0203 0906 0100160700ff 06 00000000 0202 0f00 161b + 0203 0906 0100170700ff 06 00000000 0202 0f00 161d + 0203 0906 0100180700ff 06 00000000 0202 0f00 161d + 0203 0906 0100290700ff 06 00000462 0202 0f00 161b + 0203 0906 01002a0700ff 06 00000000 0202 0f00 161b + 0203 0906 01002b0700ff 06 000005e2 0202 0f00 161d + 0203 0906 01002c0700ff 06 00000000 0202 0f00 161d + 0203 0906 01003d0700ff 06 00000000 0202 0f00 161b + 0203 0906 01003e0700ff 06 00000000 0202 0f00 161b + 0203 0906 01003f0700ff 06 00000000 0202 0f00 161d + 0203 0906 0100400700ff 06 00000000 0202 0f00 161d + 0203 0906 0100010800ff 06 00995986 0202 0f00 161e + 0203 0906 0100020800ff 06 00000008 0202 0f00 161e + 0203 0906 0100030800ff 06 0064ed4b 0202 0f00 1620 + 0203 0906 0100040800ff 06 00000005 0202 0f00 1620 +be40 7e \ No newline at end of file diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt new file mode 100644 index 0000000..ebb2d9c --- /dev/null +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt @@ -0,0 +1,41 @@ +7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 +74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b +02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d +78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 +9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 +7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 +7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 +01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f +d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 +70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b +e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 +e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 +58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 +41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 +a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 +c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f +9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe +e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 +4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 +bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 +d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d +41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 +38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec +1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f +cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 +ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 +f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 +20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc +32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 +b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 +bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 +c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c +cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 +f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e +a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 +9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 +4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 +bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 +7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 +9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d 09 ee +97 c8 31 de 81 eb dd c2 ca cb dc 76 7e diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt new file mode 100644 index 0000000..705ec66 --- /dev/null +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt @@ -0,0 +1,41 @@ +7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 +74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b +02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d +78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 +9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 +7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 +7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 +01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f +d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 +70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b +e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 +e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 +58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 +41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 +a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 +c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f +9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe +e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 +4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 +bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 +d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d +41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 +38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec +1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f +cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 +ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 +f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 +20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc +32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 +b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 +bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 +c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c +cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 +f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e +a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 +9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 +4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 +bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 +7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 +9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d bf ca +ae 62 b0 d6 69 89 48 e6 88 94 f6 0e 7e diff --git a/tests/telegrams/dlms/kamstrup-example-1.json b/tests/telegrams/dlms/kamstrup-example-1.json new file mode 100644 index 0000000..7f30a25 --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-1.json @@ -0,0 +1,61 @@ +[ + { + "hdlc": { + "raw": "7ea0e22b2113239ae6e7000f000000000c07d0010106162100ff80000102190a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff1200005be57e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "value": 39459, + "valid": true + } + }, + "crc": { + "value": 58715, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)", + "1-1:3.7.0(0)", + "1-1:4.7.0(0)" + ], + "knownObjects": [ + "1-1:96.1.1(000000000000000000)", + "1-1:1.7.0(0)", + "1-1:2.7.0(0)", + "1-1:31.7.0(0)", + "1-1:51.7.0(0)", + "1-1:71.7.0(0)", + "1-1:32.7.0(0)", + "1-1:52.7.0(0)", + "1-1:72.7.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 0, + "l3": 0 + }, + "voltage": { + "l1": 0, + "l2": 0, + "l3": 0 + } + }, + "mBus": {}, + "metadata": { + "equipmentId": "000000000000000000" + } + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-1.txt b/tests/telegrams/dlms/kamstrup-example-1.txt new file mode 100644 index 0000000..6112d6d --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-1.txt @@ -0,0 +1,17 @@ +7E A0E2 2B 21 13 239A E6E700 + 0F 00000000 0C07D0010106162100FF800001 + 0219 + 0A0E 4B616D73747275705F5630303031 + 0906 0101000005FF 0A10 35373036353637303030303030303030 + 0906 0101600101FF 0A12 303030303030303030303030303030303030 + 0906 0101010700FF 0600000000 + 0906 0101020700FF 0600000000 + 0906 0101030700FF 0600000000 + 0906 0101040700FF 0600000000 + 0906 01011F0700FF 0600000000 + 0906 0101330700FF 0600000000 + 0906 0101470700FF 0600000000 + 0906 0101200700FF 120000 + 0906 0101340700FF 120000 + 0906 0101480700FF 120000 +5BE57E \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-2.json b/tests/telegrams/dlms/kamstrup-example-2.json new file mode 100644 index 0000000..1fca444 --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-2.json @@ -0,0 +1,71 @@ +[ + { + "hdlc": { + "raw": "7ea12c2b2113fc04e6e7000f000000000c07e1081003100005ff80000002230a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000009060101020800ff060000000009060101030800ff060000000009060101040800ff0600000000c8867e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "value": 1276, + "valid": true + } + }, + "crc": { + "value": 34504, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [ + "octet_string: 07e1081003100005ff800000" + ], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)", + "1-1:3.7.0(0)", + "1-1:4.7.0(0)", + "1-1:3.8.0(0)", + "1-1:4.8.0(0)" + ], + "knownObjects": [ + "1-1:96.1.1(000000000000000000)", + "1-1:1.7.0(0)", + "1-1:2.7.0(0)", + "1-1:31.7.0(0)", + "1-1:51.7.0(0)", + "1-1:71.7.0(0)", + "1-1:32.7.0(0)", + "1-1:52.7.0(0)", + "1-1:72.7.0(0)", + "1-1:1.8.0(0)", + "1-1:2.8.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 0, + "l3": 0 + }, + "voltage": { + "l1": 0, + "l2": 0, + "l3": 0 + }, + "total": { + "received": 0, + "returned": 0 + } + }, + "mBus": {}, + "metadata": { + "equipmentId": "000000000000000000" + } + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-2.txt b/tests/telegrams/dlms/kamstrup-example-2.txt new file mode 100644 index 0000000..087e199 --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-2.txt @@ -0,0 +1,22 @@ +7E A12C 2B 21 13 FC04 E6E700 + 0F 00000000 0C07E1081003100005FF800000 + 0223 + 0A0E 4B616D73747275705F5630303031 + 0906 0101000005FF 0A10 35373036353637303030303030303030 + 0906 0101600101FF 0A12 303030303030303030303030303030303030 + 0906 0101010700FF 0600000000 + 0906 0101020700FF 0600000000 + 0906 0101030700FF 0600000000 + 0906 0101040700FF 0600000000 + 0906 01011F0700FF 0600000000 + 0906 0101330700FF 0600000000 + 0906 0101470700FF 0600000000 + 0906 0101200700FF 120000 + 0906 0101340700FF 120000 + 0906 0101480700FF 120000 + 0906 0001010000FF 090C 07E1081003100005FF800000 + 0906 0101010800FF 0600000000 + 0906 0101020800FF 0600000000 + 0906 0101030800FF 0600000000 + 0906 0101040800FF 0600000000 +C8867E \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-3.json b/tests/telegrams/dlms/kamstrup-example-3.json new file mode 100644 index 0000000..8af5852 --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-3.json @@ -0,0 +1,55 @@ +[ + { + "hdlc": { + "raw": "7ea0ae2b2113a01be6e7000f000000000c07e1081003100005ff800000020f0a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff0600000000090601011f0700ff060000000009060101200700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000005217e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "value": 7072, + "valid": true + } + }, + "crc": { + "value": 8453, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [ + "octet_string: 07e1081003100005ff800000" + ], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)" + ], + "knownObjects": [ + "1-1:96.1.1(000000000000000000)", + "1-1:1.7.0(0)", + "1-1:31.7.0(0)", + "1-1:32.7.0(0)", + "1-1:1.8.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "current": { + "l1": 0 + }, + "voltage": { + "l1": 0 + }, + "total": { + "received": 0 + } + }, + "mBus": {}, + "metadata": { + "equipmentId": "000000000000000000" + } + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-3.txt b/tests/telegrams/dlms/kamstrup-example-3.txt new file mode 100644 index 0000000..d1ea8f2 --- /dev/null +++ b/tests/telegrams/dlms/kamstrup-example-3.txt @@ -0,0 +1,12 @@ +7E A0AE 2B 21 13 A01B E6E700 + 0F 00000000 0C07E1081003100005FF800000 + 020F + 0A0E 4B616D73747275705F5630303031 + 0906 0101000005FF 0A10 35373036353637303030303030303030 + 0906 0101600101FF 0A12 303030303030303030303030303030303030 + 0906 0101010700FF 0600000000 + 0906 01011F0700FF 0600000000 + 0906 0101200700FF 120000 + 0906 0001010000FF 090C 07E1081003100005FF800000 + 0906 0101010800FF 0600000000 +05217E \ No newline at end of file diff --git a/tests/telegrams/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json similarity index 100% rename from tests/telegrams/dsmr-2.2-kfm-1.json rename to tests/telegrams/dsmr/dsmr-2.2-kfm-1.json diff --git a/tests/telegrams/dsmr-2.2-kfm-1.txt b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.txt similarity index 100% rename from tests/telegrams/dsmr-2.2-kfm-1.txt rename to tests/telegrams/dsmr/dsmr-2.2-kfm-1.txt diff --git a/tests/telegrams/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json similarity index 100% rename from tests/telegrams/dsmr-3.0-spec-example.json rename to tests/telegrams/dsmr/dsmr-3.0-spec-example.json diff --git a/tests/telegrams/dsmr-3.0-spec-example.txt b/tests/telegrams/dsmr/dsmr-3.0-spec-example.txt similarity index 100% rename from tests/telegrams/dsmr-3.0-spec-example.txt rename to tests/telegrams/dsmr/dsmr-3.0-spec-example.txt diff --git a/tests/telegrams/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json similarity index 100% rename from tests/telegrams/dsmr-4.0-isk-1.json rename to tests/telegrams/dsmr/dsmr-4.0-isk-1.json diff --git a/tests/telegrams/dsmr-4.0-isk-1.txt b/tests/telegrams/dsmr/dsmr-4.0-isk-1.txt similarity index 100% rename from tests/telegrams/dsmr-4.0-isk-1.txt rename to tests/telegrams/dsmr/dsmr-4.0-isk-1.txt diff --git a/tests/telegrams/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json similarity index 100% rename from tests/telegrams/dsmr-4.0-isk-2.json rename to tests/telegrams/dsmr/dsmr-4.0-isk-2.json diff --git a/tests/telegrams/dsmr-4.0-isk-2.txt b/tests/telegrams/dsmr/dsmr-4.0-isk-2.txt similarity index 100% rename from tests/telegrams/dsmr-4.0-isk-2.txt rename to tests/telegrams/dsmr/dsmr-4.0-isk-2.txt diff --git a/tests/telegrams/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json similarity index 100% rename from tests/telegrams/dsmr-4.0-spec-example.json rename to tests/telegrams/dsmr/dsmr-4.0-spec-example.json diff --git a/tests/telegrams/dsmr-4.0-spec-example.txt b/tests/telegrams/dsmr/dsmr-4.0-spec-example.txt similarity index 100% rename from tests/telegrams/dsmr-4.0-spec-example.txt rename to tests/telegrams/dsmr/dsmr-4.0-spec-example.txt diff --git a/tests/telegrams/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json similarity index 100% rename from tests/telegrams/dsmr-4.2-kfm-1.json rename to tests/telegrams/dsmr/dsmr-4.2-kfm-1.json diff --git a/tests/telegrams/dsmr-4.2-kfm-1.txt b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.txt similarity index 100% rename from tests/telegrams/dsmr-4.2-kfm-1.txt rename to tests/telegrams/dsmr/dsmr-4.2-kfm-1.txt diff --git a/tests/telegrams/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json similarity index 100% rename from tests/telegrams/dsmr-4.2-xmx-1.json rename to tests/telegrams/dsmr/dsmr-4.2-xmx-1.json diff --git a/tests/telegrams/dsmr-4.2-xmx-1.txt b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.txt similarity index 100% rename from tests/telegrams/dsmr-4.2-xmx-1.txt rename to tests/telegrams/dsmr/dsmr-4.2-xmx-1.txt diff --git a/tests/telegrams/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json similarity index 100% rename from tests/telegrams/dsmr-4.2.2-spec-example.json rename to tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json diff --git a/tests/telegrams/dsmr-4.2.2-spec-example.txt b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.txt similarity index 100% rename from tests/telegrams/dsmr-4.2.2-spec-example.txt rename to tests/telegrams/dsmr/dsmr-4.2.2-spec-example.txt diff --git a/tests/telegrams/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-1.json rename to tests/telegrams/dsmr/dsmr-5.0-ene-1.json diff --git a/tests/telegrams/dsmr-5.0-ene-1.txt b/tests/telegrams/dsmr/dsmr-5.0-ene-1.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-1.txt rename to tests/telegrams/dsmr/dsmr-5.0-ene-1.txt diff --git a/tests/telegrams/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-2.json rename to tests/telegrams/dsmr/dsmr-5.0-ene-2.json diff --git a/tests/telegrams/dsmr-5.0-ene-2.txt b/tests/telegrams/dsmr/dsmr-5.0-ene-2.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-2.txt rename to tests/telegrams/dsmr/dsmr-5.0-ene-2.txt diff --git a/tests/telegrams/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-3.json rename to tests/telegrams/dsmr/dsmr-5.0-ene-3.json diff --git a/tests/telegrams/dsmr-5.0-ene-3.txt b/tests/telegrams/dsmr/dsmr-5.0-ene-3.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-ene-3.txt rename to tests/telegrams/dsmr/dsmr-5.0-ene-3.txt diff --git a/tests/telegrams/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json similarity index 100% rename from tests/telegrams/dsmr-5.0-est-units.json rename to tests/telegrams/dsmr/dsmr-5.0-est-units.json diff --git a/tests/telegrams/dsmr-5.0-est-units.txt b/tests/telegrams/dsmr/dsmr-5.0-est-units.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-est-units.txt rename to tests/telegrams/dsmr/dsmr-5.0-est-units.txt diff --git a/tests/telegrams/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json similarity index 100% rename from tests/telegrams/dsmr-5.0-isk-1.json rename to tests/telegrams/dsmr/dsmr-5.0-isk-1.json diff --git a/tests/telegrams/dsmr-5.0-isk-1.txt b/tests/telegrams/dsmr/dsmr-5.0-isk-1.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-isk-1.txt rename to tests/telegrams/dsmr/dsmr-5.0-isk-1.txt diff --git a/tests/telegrams/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json similarity index 100% rename from tests/telegrams/dsmr-5.0-spec-example-lowercase.json rename to tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json diff --git a/tests/telegrams/dsmr-5.0-spec-example-lowercase.txt b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-spec-example-lowercase.txt rename to tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.txt diff --git a/tests/telegrams/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json similarity index 100% rename from tests/telegrams/dsmr-5.0-spec-example.json rename to tests/telegrams/dsmr/dsmr-5.0-spec-example.json diff --git a/tests/telegrams/dsmr-5.0-spec-example.txt b/tests/telegrams/dsmr/dsmr-5.0-spec-example.txt similarity index 100% rename from tests/telegrams/dsmr-5.0-spec-example.txt rename to tests/telegrams/dsmr/dsmr-5.0-spec-example.txt diff --git a/tests/telegrams/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json similarity index 100% rename from tests/telegrams/dsmr-luxembourgh-spec-example.json rename to tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json diff --git a/tests/telegrams/dsmr-luxembourgh-spec-example.txt b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.txt similarity index 100% rename from tests/telegrams/dsmr-luxembourgh-spec-example.txt rename to tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.txt diff --git a/tests/telegrams/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json similarity index 100% rename from tests/telegrams/emucs-p1-v2.1.1-spec-example-1.json rename to tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json diff --git a/tests/telegrams/emucs-p1-v2.1.1-spec-example-1.txt b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.txt similarity index 100% rename from tests/telegrams/emucs-p1-v2.1.1-spec-example-1.txt rename to tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.txt diff --git a/tests/telegrams/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json similarity index 100% rename from tests/telegrams/emucs-p1-v2.1.1-spec-example-2.json rename to tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json diff --git a/tests/telegrams/emucs-p1-v2.1.1-spec-example-2.txt b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.txt similarity index 100% rename from tests/telegrams/emucs-p1-v2.1.1-spec-example-2.txt rename to tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.txt diff --git a/tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt similarity index 100% rename from tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt rename to tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt diff --git a/tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt similarity index 100% rename from tests/telegrams/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt rename to tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt diff --git a/tests/telegrams/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json similarity index 100% rename from tests/telegrams/iskra-mt-382-no-crc-with-text-message.json rename to tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json diff --git a/tests/telegrams/iskra-mt-382-no-crc-with-text-message.txt b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.txt similarity index 100% rename from tests/telegrams/iskra-mt-382-no-crc-with-text-message.txt rename to tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.txt diff --git a/tests/telegrams/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json similarity index 100% rename from tests/telegrams/iskra-mt-382-no-crc.json rename to tests/telegrams/dsmr/iskra-mt-382-no-crc.json diff --git a/tests/telegrams/iskra-mt-382-no-crc.txt b/tests/telegrams/dsmr/iskra-mt-382-no-crc.txt similarity index 100% rename from tests/telegrams/iskra-mt-382-no-crc.txt rename to tests/telegrams/dsmr/iskra-mt-382-no-crc.txt diff --git a/tests/telegrams/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json similarity index 100% rename from tests/telegrams/kamstrup-OMNIA-e-meter-three-phase.json rename to tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json diff --git a/tests/telegrams/kamstrup-OMNIA-e-meter-three-phase.txt b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.txt similarity index 100% rename from tests/telegrams/kamstrup-OMNIA-e-meter-three-phase.txt rename to tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.txt diff --git a/tests/telegrams/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json similarity index 100% rename from tests/telegrams/sagemcom-xt211.json rename to tests/telegrams/dsmr/sagemcom-xt211.json diff --git a/tests/telegrams/sagemcom-xt211.txt b/tests/telegrams/dsmr/sagemcom-xt211.txt similarity index 100% rename from tests/telegrams/sagemcom-xt211.txt rename to tests/telegrams/dsmr/sagemcom-xt211.txt diff --git a/tests/telegrams/unknown-xmx-1.json b/tests/telegrams/dsmr/unknown-xmx-1.json similarity index 100% rename from tests/telegrams/unknown-xmx-1.json rename to tests/telegrams/dsmr/unknown-xmx-1.json diff --git a/tests/telegrams/unknown-xmx-1.txt b/tests/telegrams/dsmr/unknown-xmx-1.txt similarity index 100% rename from tests/telegrams/unknown-xmx-1.txt rename to tests/telegrams/dsmr/unknown-xmx-1.txt diff --git a/tests/telegrams/m-bus/austria-decrypted-1.txt b/tests/telegrams/m-bus/austria-decrypted-1.txt new file mode 100644 index 0000000..93f6c28 --- /dev/null +++ b/tests/telegrams/m-bus/austria-decrypted-1.txt @@ -0,0 +1 @@ +0F8006870E0C07E5091B01092F0F00FF88800223090C07E5091B01092F0F00FF888009060100010800FF060000328902020F00161E09060100020800FF060000000002020F00161E09060100010700FF060000000002020F00161B09060100020700FF060000000002020F00161B09060100200700FF12092102020FFF162309060100340700FF12000002020FFF162309060100480700FF12000002020FFF1623090601001F0700FF12000002020FFE162109060100330700FF12000002020FFE162109060100470700FF12000002020FFE1621090601000D0700FF1203E802020FFD16FF090C313831323230303030303039 \ No newline at end of file diff --git a/tests/telegrams/m-bus/austria-example-1 copy.txt b/tests/telegrams/m-bus/austria-example-1 copy.txt new file mode 100644 index 0000000..d62787d --- /dev/null +++ b/tests/telegrams/m-bus/austria-example-1 copy.txt @@ -0,0 +1,8 @@ +DB 08 + 4B464D6750000009 + 81 F8 + 20 + 00000023 + +88D5AB4F97515AAFC6B88D2F85DAA7A0E3C0C40D004535C397C9D037AB7DBDA329107615444894A1A0DD7E85F02D496CECD3FF46AF5F B3C9229CFE8F3EE4606AB2E1F409F36AAD2E50900A4396FC6C2E083F373233A69616950758BFC7D63A9E9B6E99E21B2CBC2B934772CA51 FD4D69830711CAB1F8CFF25F0A329337CBA51904F0CAED88D61968743C8454BA922EB00038182C22FE316D16F2A9F544D6F75D51A4E92 A1C4EF8AB19A2B7FEAA32D0726C0ED80229AE6C0F7621A4209251ACE2B2BC66FF0327A653BB686C756BE033C7A281F1D2A7E1FA31C398 +3E15F8FD16CC5787E6F5419A3CFDA44BE438C96F0E38BF83D9 \ No newline at end of file diff --git a/tests/telegrams/m-bus/austria-example-1.txt b/tests/telegrams/m-bus/austria-example-1.txt new file mode 100644 index 0000000..b930a2c --- /dev/null +++ b/tests/telegrams/m-bus/austria-example-1.txt @@ -0,0 +1,19 @@ +68 FA FA 68 +53 FF 00 01 67 + +DB 08 + 4B464D6750000009 + 81 F8 + 20 + 00000023 + +88 D5 AB 4F 97 51 5A AF C6 B8 8D 2F 85 DA A7 A0 E3 C0 C4 0D 00 45 35 C3 97 C9 D0 +37 AB 7D BD A3 29 10 76 15 44 48 94 A1 A0 DD 7E 85 F0 2D 49 6C EC D3 FF 46 AF 5F +B3 C9 22 9C FE 8F 3E E4 60 6A B2 E1 F4 09 F3 6A AD 2E 50 90 0A 43 96 FC 6C 2E 08 +3F373233A69616950758BFC7D63A9E9B6E99E21B2CBC2B934772CA51FD4D69830711CAB1F8CFF25F0A329337CBA51904F0CA +ED88D61968743C8454BA922EB00038182C22FE316D16F2A9F544D6F75D51A4E92A1C4EF8AB19A2B7FEAA32D0726C0ED80229AE6C +0F7621A4209251ACE2B2BC66FF0327A653BB686C756BE033C7A281F1D2A7E1FA31C3983E15F8FD16CC5787E6F517166814146853F +F110167419A3CFDA44BE438C96F0E38BF83D98316 + +36C66639E48A8CA4D6BC8B282A793BBB +36C66639E48A8CA4D6BC8B282A793BBB \ No newline at end of file diff --git a/tests/test-utils.ts b/tests/test-utils.ts index d79f496..c1b97a4 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -1,18 +1,26 @@ import crypto from 'node:crypto'; import { promises as fs } from 'node:fs'; import { - ENCRYPTED_DSMR_GCM_TAG_LEN, - ENCRYPTED_DSMR_HEADER_LEN, - ENCRYPTED_DSMR_CONTENT_LENGTH_START, - ENCRYPTED_DSMR_SYSTEM_TITLE_LEN, - ENCRYPTED_DSMR_TELEGRAM_SOF, -} from '../src/util/encryption.js'; + ENCRYPTED_DLMS_GCM_TAG_LEN, + ENCRYPTED_DLMS_HEADER_LEN, + ENCRYPTED_DLMS_SYSTEM_TITLE_LEN, + ENCRYPTED_DLMS_TELEGRAM_SOF, +} from '../src/protocols/encryption.js'; +import { HDLC_TELEGRAM_SOF_EOF, HDLC_LLC_DESTINATION, HDLC_LLC_SOURCE, HDLC_LLC_QUALITY, HDLC_FORMAT_START } from '../src/protocols/hdlc.js'; +import { calculateCrc16IbmSdlc } from '../src/util/crc.js'; export const TEST_DECRYPTION_KEY = Buffer.from('0123456789abcdef01234567890abcdef', 'hex'); export const TEST_AAD = Buffer.from('ffeeddccbbaa99887766554433221100', 'hex'); +export const DSMR_TEST_FOLDER = './tests/telegrams/dsmr'; +export const DLMS_TEST_FOLDER = './tests/telegrams/dlms'; -export const getAllTestTelegramTestCases = async () => { - const files = await fs.readdir('./tests/telegrams'); +export const getAllDSMRTestTelegramTestCases = async () => { + const files = await fs.readdir(DSMR_TEST_FOLDER); + return files.filter((file) => file.endsWith('.txt')).map((file) => file.replace('.txt', '')); +}; + +export const getAllDLMSTestTelegramTestCases = async () => { + const files = await fs.readdir(DLMS_TEST_FOLDER); return files.filter((file) => file.endsWith('.txt')).map((file) => file.replace('.txt', '')); }; @@ -29,7 +37,22 @@ export const readTelegramFromFiles = async (path: string, replaceNewLines = true export const readHexFile = async (path: string) => { const file = await fs.readFile(path, 'utf-8'); - return Buffer.from(file.replace(/\s/g, ''), 'hex'); + // Replace all comments in the file + const cleanedFile = file.replace(/#.*$/gm, ''); + const cleanedFile2 = cleanedFile.replace(/\/\/.*$/gm, ''); + + return Buffer.from(cleanedFile2.replace(/\s/g, ''), 'hex'); +}; + +export const writeHexFile = async (path: string, data: Buffer) => { + const hexString = bufferToHexString(data); + await fs.writeFile(path, hexString); + + return hexString; +}; + +export const numToHex = (num: number, minDigits = 2) => { + return `0x${num.toString(16).padStart(minDigits, '0')}`; }; export const chunkString = (str: string, chunkSize: number) => { @@ -56,12 +79,64 @@ export const bufferToHexString = (buffer: Buffer) => { let hexString = ''; for (let i = 0; i < buffer.length; i += 16) { - hexString += buffer.subarray(i, i + 16).toString('hex') + '\n'; + hexString += + buffer + .subarray(i, i + 16) + .toString('hex') + .match(/.{1,2}/g) + ?.join(' ') + '\n'; } return hexString; }; +export const wrapHdlcFrame = (frame: Buffer) => { + const hdlcHeader = Buffer.from([ + HDLC_TELEGRAM_SOF_EOF, // 0: SOF + 0x00, // 1: Format type + length + 0x00, // 2: Length + 0x03, // 3: Destination address, + 0x05, // 4: Source Address, + 0x00, // 5: Control byte, + 0x00, // 6: Checksum + 0x00, // 7: Checksum, + HDLC_LLC_DESTINATION, // 8: LLC Destination + HDLC_LLC_SOURCE, // 9: LLC Source + HDLC_LLC_QUALITY, // 10: LLC Quality + ]); + + const hdlcFooter = Buffer.from([ + 0x00, // Checksum + 0x00, // Checksum + HDLC_TELEGRAM_SOF_EOF, + ]); + + // Frame length is total length - 2 (SOF and EOF) + const frameLength = frame.length + hdlcHeader.length + hdlcFooter.length - 2; + + if (frameLength > 0x7ff) { + throw new Error('Frame length is too long to fit in HDLC'); + } + + // Leave segmentation bit to 0. + hdlcHeader[1] = (HDLC_FORMAT_START << 4) | ((frameLength >> 8) & 0x07); + hdlcHeader[2] = frameLength & 0xff; + + // Don't include SOF in the checksum calculation + const headerChecksum = calculateCrc16IbmSdlc(hdlcHeader.subarray(1, 6)); + + hdlcHeader.writeUint16LE(headerChecksum, 6); + + const frameUntilFooter = Buffer.concat([hdlcHeader, frame]); + + // Don't include SOF in the checksum calculation + const footerChecksum = calculateCrc16IbmSdlc(frameUntilFooter.subarray(1)); + + hdlcFooter.writeUint16LE(footerChecksum, 0); + + return Buffer.concat([frameUntilFooter, hdlcFooter]); +} + export const encryptFrame = ({ frame, key, @@ -82,7 +157,7 @@ export const encryptFrame = ({ const iv = Buffer.concat([systemTitle, frameCounter]); const cipher = crypto.createCipheriv('aes-128-gcm', key, iv, { - authTagLength: ENCRYPTED_DSMR_GCM_TAG_LEN, + authTagLength: ENCRYPTED_DLMS_GCM_TAG_LEN, }); if (aad?.length == 16) { @@ -97,16 +172,17 @@ export const encryptFrame = ({ const gcmTag = cipher.getAuthTag(); const result = Buffer.alloc( - ENCRYPTED_DSMR_HEADER_LEN + encryptedFrame.length + ENCRYPTED_DSMR_GCM_TAG_LEN, + ENCRYPTED_DLMS_HEADER_LEN + encryptedFrame.length + ENCRYPTED_DLMS_GCM_TAG_LEN, ); let index = 0; - result.writeUint8(ENCRYPTED_DSMR_TELEGRAM_SOF, index++); - result.writeUint8(ENCRYPTED_DSMR_SYSTEM_TITLE_LEN, index++); + result.writeUint8(ENCRYPTED_DLMS_TELEGRAM_SOF, index++); + result.writeUint8(ENCRYPTED_DLMS_SYSTEM_TITLE_LEN, index++); systemTitle.copy(result, index); - index += ENCRYPTED_DSMR_SYSTEM_TITLE_LEN; - result.writeUInt8(ENCRYPTED_DSMR_CONTENT_LENGTH_START, index++); - result.writeUint16BE(encryptedFrame.length + ENCRYPTED_DSMR_HEADER_LEN - 1, index); + index += ENCRYPTED_DLMS_SYSTEM_TITLE_LEN; + // 0x82 is indicating that a 16 byte length follows. + result.writeUInt8(0x82, index++); + result.writeUint16BE(encryptedFrame.length + ENCRYPTED_DLMS_HEADER_LEN - 1, index); index += 2; result.writeUInt8(0x30, index++); frameCounter.copy(result, index); diff --git a/tests/util/crc.spec.ts b/tests/util/crc.spec.ts new file mode 100644 index 0000000..e1516d4 --- /dev/null +++ b/tests/util/crc.spec.ts @@ -0,0 +1,73 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { calculateCrc16Arc, calculateCrc16IbmSdlc } from '../../src/util/crc.js'; + +describe('CRC', () => { + describe('CRC-16/ARC', () => { + // Note: these cases have been verified using https://crccalc.com/ + const CRC_TESTS = [ + { + input: 'Hello, world!', + output: 0x9a4a, + }, + { + input: '', + output: 0x0000, + }, + { + input: '123456789', + output: 0xbb3d, + }, + { + input: 'The quick brown fox jumps over the lazy dog', + output: 0xfcdf, + }, + { + input: 'Lorem ipsum dolor sit amet', + output: 0xc14f, + }, + ]; + + for (const test of CRC_TESTS) { + it(`Calculates the CRC of "${test.input}"`, () => { + const buf = Buffer.from(test.input); + const crc = calculateCrc16Arc(buf); + assert.equal(crc, test.output); + }); + } + }); + + describe('CRC-16/IBM-SDLC', () => { + // Note: these cases have been verified using https://crccalc.com/ + const CRC_TESTS = [ + { + input: 'Hello, world!', + output: 0x1eb5, + }, + { + input: '', + output: 0x0000, + }, + { + input: '123456789', + output: 0x906e, + }, + { + input: 'The quick brown fox jumps over the lazy dog', + output: 0x9358, + }, + { + input: 'Lorem ipsum dolor sit amet', + output: 0x7ff8, + }, + ]; + + for (const test of CRC_TESTS) { + it(`Calculates the CRC of "${test.input}"`, () => { + const buf = Buffer.from(test.input); + const crc = calculateCrc16IbmSdlc(buf); + assert.equal(crc, test.output); + }); + } + }); +}); diff --git a/tools/decrypt-telegram.ts b/tools/decrypt-telegram.ts index 9b0c59e..8c2e962 100644 --- a/tools/decrypt-telegram.ts +++ b/tools/decrypt-telegram.ts @@ -3,7 +3,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { TEST_AAD, TEST_DECRYPTION_KEY } from '../tests/test-utils.js'; -import { decryptFrame } from '../src/util/encryption.js'; +import { decryptDlmsFrame } from '../src/protocols/encryption.js'; const inputPath = process.argv[2]; const outputPath = process.argv[3]; @@ -43,18 +43,17 @@ if (isHexFile) { file = Buffer.from(fileString.replace(/\s/g, ''), 'hex'); } -const { header, footer, content } = decryptFrame({ +const { header, footer, content, error } = decryptDlmsFrame({ data: file, key: Buffer.from(decryptionKey, 'hex'), additionalAuthenticatedData: Buffer.from(aad, 'hex'), - encoding: 'binary', }); const resolvedOutputPath = path.resolve(process.cwd(), outputPath); await fs.writeFile(resolvedOutputPath, content, 'binary'); -console.log('Telegram decrypted successfully'); +console.log('Telegram decrypted'); console.log('Header fields:'); console.log(' - System title:', header.systemTitle.toString('hex')); console.log(' - Frame counter:', header.frameCounter.toString('hex')); @@ -63,4 +62,5 @@ console.log(' - Content length:', header.contentLength); console.log('Footer fields:'); console.log(' - GCM Tag:', footer.gcmTag.toString('hex')); console.log(); +console.log('Decryption error:', error ? error.message : 'none'); console.log(`Decrypted telegram written to ${resolvedOutputPath}`); diff --git a/tools/parse-dlms.ts b/tools/parse-dlms.ts new file mode 100644 index 0000000..d6e512b --- /dev/null +++ b/tools/parse-dlms.ts @@ -0,0 +1,89 @@ +/* eslint-disable no-console */ +import path from 'node:path'; +import { inspect } from 'node:util'; + +import { bufferToHexString, numToHex, readHexFile } from '../tests/test-utils.js'; +import { decodeDLMSContent, decodeDlmsObis } from '../src/protocols/dlms.js'; +import { isDlmsStructureLike, ParsedDlmsData } from '../src/protocols/dlms-datatype.js'; +import { obisCodeToString, parseObisCodeFromBuffer } from '../src/protocols/obis-code.js'; + +const filePath = process.argv[2]; + +if (filePath === undefined) { + console.error('Please provide a file path as argument.'); + console.log('Usage:'); + console.log('npm run tool:parse-hdlc '); + process.exit(1); +} + +const decryptionKey = process.argv[3]; +const aad = process.argv[4]; + +const resolvedPath = path.resolve(process.cwd(), filePath); + +const file = await readHexFile(resolvedPath); +console.log('Content Raw:'); +console.log(bufferToHexString(file)); + +const dlmsDataTypeToList = (object: ParsedDlmsData, prefix: string) => { + let result = ''; + if (isDlmsStructureLike(object)) { + result += `${prefix}- ${object.type}:\n`; + + for (const item of object.value) { + result += dlmsDataTypeToList(item, `${prefix} `); + } + } else if (Buffer.isBuffer(object.value)) { + const { obisCode } = parseObisCodeFromBuffer(object.value); + let obisCodeString = ''; + + if (obisCode) { + obisCodeString = ` (${obisCodeToString(obisCode)})`; + } + + result += `${prefix}- ${object.type}: ${object.value.toString('hex')}${obisCodeString}\n`; + } else { + result += `${prefix}- ${object.type}: ${inspect(object.value)}\n`; + } + + return result; +}; + +const objectToList = (object: object, prefix: string) => { + let result = ''; + for (const [key, value] of Object.entries(object)) { + if (Buffer.isBuffer(value)) { + result += `${prefix}- ${key}: ${value.toString('hex')}\n`; + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + result += `${prefix}- ${key}:\n`; + result += objectToList(value as object, `${prefix} `); + } else { + result += `${prefix}- ${key}: ${String(value)}\n`; + } + } + return result; +}; + +const dlmsContent = decodeDLMSContent({ + frame: file, + decryptionKey: decryptionKey ? Buffer.from(decryptionKey, 'hex') : undefined, + additionalAuthenticatedData: aad ? Buffer.from(aad, 'hex') : undefined, +}); + +console.log('Content DLMS:'); +console.log(` - Invoke ID: ${numToHex(dlmsContent.invokeId, 8)}`); +console.log(` - Timestamp: ${dlmsContent.timestamp.toString('hex')}`); +console.log(` - DLMS Data:`); +console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); + +const energyContent = decodeDlmsObis(dlmsContent); +delete energyContent.hdlc; + +console.log('Content Parsed:'); +if (energyContent.dlms?.unknownObjects) { + console.log('Unknown Objects:'); + console.log(objectToList(energyContent.dlms?.unknownObjects, ' ')); + console.log('Parsed Objects:'); +} +delete energyContent.dlms; +console.log(objectToList(energyContent, ' ')); diff --git a/tools/parse-hdlc.ts b/tools/parse-hdlc.ts new file mode 100644 index 0000000..d7d9e80 --- /dev/null +++ b/tools/parse-hdlc.ts @@ -0,0 +1,115 @@ +/* eslint-disable no-console */ +import path from 'node:path'; +import { inspect } from 'node:util'; + +import { + decodeHdlcFooter, + decodeHdlcHeader, + decodeLlcHeader, + HDLC_FOOTER_LENGTH, +} from '../src/protocols/hdlc.js'; +import { bufferToHexString, numToHex, readHexFile } from '../tests/test-utils.js'; +import { decodeDLMSContent, decodeDlmsObis } from '../src/protocols/dlms.js'; +import { isDlmsStructureLike, ParsedDlmsData } from '../src/protocols/dlms-datatype.js'; + +const filePath = process.argv[2]; + +if (filePath === undefined) { + console.error('Please provide a file path as argument.'); + console.log('Usage:'); + console.log('npm run tool:parse-hdlc '); + process.exit(1); +} + +const decryptionKey = process.argv[3]; +const aad = process.argv[4]; + +const resolvedPath = path.resolve(process.cwd(), filePath); + +const file = await readHexFile(resolvedPath); + +const header = decodeHdlcHeader(file); +console.log('Header:'); +console.log(` - Format Type: ${numToHex(header.formatType)}`); +console.log(` - Segmentation: ${header.segmentation}`); +console.log(` - Length: ${header.frameLength}`); +console.log(` - Checksum: ${numToHex(header.crc)} (valid: ${header.crcValid})`); +console.log(` - Control byte: ${numToHex(header.controlByte)}`); +console.log(); + +// Entire frame including SOF and EOF +const frame = file.subarray(0, header.frameLength + 2); +const frameContent = frame.subarray(header.consumedBytes); + +const llc = decodeLlcHeader(frameContent); +console.log('LLC Header:'); +console.log(` - Destination: ${numToHex(llc.destination)}`); +console.log(` - Source: ${numToHex(llc.source)}`); +console.log(` - Quality: ${numToHex(llc.quality)}`); +console.log(); + +const content = frame.subarray( + header.consumedBytes + llc.consumedBytes, + frame.length - HDLC_FOOTER_LENGTH, +); +console.log('Content Raw:'); +console.log(bufferToHexString(content)); + +const dlmsDataTypeToList = (object: ParsedDlmsData, prefix: string) => { + let result = ''; + if (isDlmsStructureLike(object)) { + result += `${prefix}- ${object.type}:\n`; + + for (const item of object.value) { + result += dlmsDataTypeToList(item, `${prefix} `); + } + } else if (Buffer.isBuffer(object.value)) { + result += `${prefix}- ${object.type}: ${object.value.toString('hex')}\n`; + } else { + result += `${prefix}- ${object.type}: ${inspect(object.value)}\n`; + } + + return result; +}; + +const objectToList = (object: object, prefix: string) => { + let result = ''; + for (const [key, value] of Object.entries(object)) { + if (Buffer.isBuffer(value)) { + result += `${prefix}- ${key}: ${value.toString('hex')}\n`; + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + result += `${prefix}- ${key}:\n`; + result += objectToList(value as object, `${prefix} `); + } else { + result += `${prefix}- ${key}: ${String(value)}\n`; + } + } + return result; +}; + +const dlmsContent = decodeDLMSContent({ + frame: content, + decryptionKey: decryptionKey ? Buffer.from(decryptionKey, 'hex') : undefined, + additionalAuthenticatedData: aad ? Buffer.from(aad, 'hex') : undefined, +}); +console.log('Content DLMS:'); +console.log(` - Invoke ID: ${numToHex(dlmsContent.invokeId, 8)}`); +console.log(` - Timestamp: ${dlmsContent.timestamp.toString('hex')}`); +console.log(` - DLMS Data:`); +console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); + +const energyContent = decodeDlmsObis(dlmsContent); +delete energyContent.hdlc; + +console.log('Content Parsed:'); +if (energyContent.dlms?.unknownObjects) { + console.log('Unknown Objects:'); + console.log(objectToList(energyContent.dlms?.unknownObjects, ' ')); + console.log('Parsed Objects:'); +} +delete energyContent.dlms; +console.log(objectToList(energyContent, ' ')); + +const footer = decodeHdlcFooter(frame); +console.log('Footer:'); +console.log(` - CRC: ${numToHex(footer.crc)} (valid: ${footer.crcValid})`); diff --git a/tools/parse-telegram.ts b/tools/parse-telegram.ts index 86464be..455da48 100644 --- a/tools/parse-telegram.ts +++ b/tools/parse-telegram.ts @@ -1,10 +1,15 @@ /* eslint-disable no-console */ /** This script is used to parse a DSMR telegram from a file. */ import { promises as fs } from 'node:fs'; -import { inspect } from 'node:util'; import path from 'node:path'; import { PassThrough } from 'node:stream'; -import { DSMR, DSMRDecryptionError, DSMRError } from '../src/index.js'; + +import { SmartMeterDecryptionError, SmartMeterError } from '../src/index.js'; +import { StreamDetectType } from '../src/stream/stream-detect-type.js'; +import { DlmsStreamParser } from '../src/stream/stream-dlms.js'; +import { EncryptedDSMRStreamParser } from '../src/stream/stream-encrypted-dsmr.js'; +import { SmartMeterStreamCallback, SmartMeterStreamParser } from '../src/stream/stream.js'; +import { UnencryptedDSMRStreamParser } from '../src/stream/stream-unencrypted-dsmr.js'; const filePath = process.argv[2]; @@ -30,42 +35,100 @@ if (isHexFile) { console.log('Hex file detected'); console.log({ file: file.toString() }); + console.log(file.toString()); } const passthrough = new PassThrough(); -// Stream is not necessary for this script, but it allows to detect encryption in the frame. -DSMR.createStreamParser({ - stream: passthrough, - newLineChars: isHexFile ? '\r\n' : '\n', // Use CRLF for hex files as thats what used by meters - decryptionKey: decryptionKey ? Buffer.from(decryptionKey, 'hex') : undefined, - additionalAuthenticatedData: aad ? Buffer.from(aad, 'hex') : undefined, - detectEncryption: true, - fullFrameRequiredWithinMs: 100, - callback: (error, result) => { - const inspected = inspect(result, { - depth: null, // Infinite depth - colors: true, +const waitForFrameDetection = () => { + return new Promise<{ + mode: 'dsmr' | 'dlms'; + encrypted: boolean; + data: Buffer; + }>((resolve) => { + const detector = new StreamDetectType({ + stream: passthrough, + callback: (result) => { + detector.destroy(); + resolve(result); + }, }); - if (error instanceof DSMRDecryptionError) { - console.error('Decryption error:', error.message); - console.error('Original error:', error.cause); - console.log('Telegram:', error.rawTelegram?.toString('hex')); - } else if (error instanceof DSMRError) { - console.error(error.message); - console.log(error.rawTelegram?.toString('hex')); - } else if (error) { - console.error(error); - } else if (result?.crc?.valid === false) { - console.log('CRC validation failed'); - console.log(inspected); - } else { - console.log(inspected); + passthrough.write(file); + }); +}; + +const { mode, encrypted, data } = await waitForFrameDetection(); + +console.log('Detected frame:'); +console.log(` - Mode: ${mode}`); +console.log(` - Encrypted: ${encrypted}`); + +if (encrypted && !decryptionKey) { + console.error('Decryption key is required for encrypted frames'); + process.exit(1); +} + +const callback: SmartMeterStreamCallback = (error, result) => { + if (error instanceof SmartMeterDecryptionError) { + console.error('Decryption error:', error.message); + console.error('Original error:', error.cause); + console.log('Telegram:', error.rawTelegram?.toString('hex')); + } else if (error instanceof SmartMeterError) { + console.error(error.message); + console.log(error.rawTelegram?.toString('hex')); + } else if (error) { + console.error(error); + } else if (!result) { + console.error('No result and no error'); + } else { + let crcValid = true; + if ('hdlc' in result) { + const msgCrcValid = result.hdlc.crc?.valid === false; + const hdrCrcValid = result.hdlc.header.crc?.valid === false; + crcValid = msgCrcValid && hdrCrcValid; + } else if ('dsmr' in result) { + crcValid = result.dsmr.crc?.valid === false; + } + + if (!crcValid) { + console.error('CRC validation failed'); } + console.dir(result, { depth: null }); + } +}; - process.exit(0); - }, -}); +let parser: SmartMeterStreamParser; + +if (mode === 'dlms' && !encrypted) { + parser = new DlmsStreamParser({ + stream: passthrough, + callback, + }); +} else if (mode === 'dlms' && encrypted) { + parser = new DlmsStreamParser({ + stream: passthrough, + callback, + decryptionKey: Buffer.from(decryptionKey, 'hex'), + additionalAuthenticatedData: aad ? Buffer.from(aad, 'hex') : undefined, + }); +} else if (mode === 'dsmr' && !encrypted) { + parser = new UnencryptedDSMRStreamParser({ + stream: passthrough, + callback, + }); +} else if (mode === 'dsmr' && encrypted) { + parser = new EncryptedDSMRStreamParser({ + stream: passthrough, + decryptionKey: Buffer.from(decryptionKey, 'hex'), + additionalAuthenticatedData: aad ? Buffer.from(aad, 'hex') : undefined, + callback, + }); +} else { + console.error('Unknown mode'); + process.exit(1); +} -passthrough.write(file); +passthrough.write(data); +passthrough.end(); +parser.destroy(); diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index 0e94a4c..9e2c049 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -5,55 +5,147 @@ * DSMRParser. */ import fs from 'fs/promises'; -import { DSMR } from '../src/index.js'; import { bufferToHexString, encryptFrame, - getAllTestTelegramTestCases, + getAllDLMSTestTelegramTestCases, + getAllDSMRTestTelegramTestCases, + readHexFile, TEST_AAD, TEST_DECRYPTION_KEY, + wrapHdlcFrame, + writeHexFile, } from '../tests/test-utils.js'; +import { DlmsStreamParser } from '../src/stream/stream-dlms.js'; +import { PassThrough } from 'stream'; +import { parseDsmr } from '../src/protocols/dsmr.js'; +import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/protocols/hdlc.js'; -const testCases = await getAllTestTelegramTestCases(); +// Parse all DSMR telegrams +{ + const testCases = await getAllDSMRTestTelegramTestCases(); + + for (const file of testCases) { + let input = await fs.readFile(`./tests/telegrams/dsmr/${file}.txt`, 'utf-8'); + input = input.replace(/\r?\n/g, '\r\n'); + console.log(`Parsing ${file}.txt`); + const parsed = parseDsmr({ telegram: input }); + const json = JSON.stringify(parsed, null, 2); + await fs.writeFile(`./tests/telegrams/dsmr/${file}.json`, json); + } +} -for (const file of testCases) { - let input = await fs.readFile(`./tests/telegrams/${file}.txt`, 'utf-8'); +// Encrypted DSMR frames +{ + const fileToEncrypt = 'dsmr-luxembourgh-spec-example'; + console.log(`Using ${fileToEncrypt} as test case for encrypted DSMR telegrams`); + + let input = await fs.readFile(`./tests/telegrams/dsmr/${fileToEncrypt}.txt`, 'utf-8'); input = input.replace(/\r?\n/g, '\r\n'); - console.log(`Parsing ${file}.txt`); - const parsed = DSMR.parse({ telegram: input }); - const json = JSON.stringify(parsed, null, 2); - await fs.writeFile(`./tests/telegrams/${file}.json`, json); + + const encryptedAad = encryptFrame({ + frame: input, + key: TEST_DECRYPTION_KEY, + aad: TEST_AAD, + }); + + await writeHexFile( + `./tests/telegrams/dsmr/encrypted/${fileToEncrypt}-with-aad.txt`, + encryptedAad, + ); + + const encryptedWithoutAad = encryptFrame({ + frame: input, + key: TEST_DECRYPTION_KEY, + }); + + await writeHexFile( + `./tests/telegrams/dsmr/encrypted/${fileToEncrypt}-without-aad.txt`, + encryptedWithoutAad, + ); } -const fileToEncrypt = 'dsmr-luxembourgh-spec-example'; -console.log(`Using ${fileToEncrypt} as test case for encrypted telegrams`); +// Parse all DLMS telegrams +{ + const dlmsTestCases = await getAllDLMSTestTelegramTestCases(); + + for (const file of dlmsTestCases) { + console.log(`Parsing ${file}.txt`); + const input = await readHexFile(`./tests/telegrams/dlms/${file}.txt`); + + const passthrough = new PassThrough(); -let input = await fs.readFile(`./tests/telegrams/${fileToEncrypt}.txt`, 'utf-8'); -input = input.replace(/\r?\n/g, '\r\n'); + const results: object[] = []; + + const parser = new DlmsStreamParser({ + stream: passthrough, + callback: (error, result) => { + if (error) { + if (error instanceof Error) { + results.push({ + error: { + message: error.message, + name: error.name, + stack: error.stack, + }, + }); + } else { + results.push({ + error, + }); + } + } else if (result) { + results.push(result); + } + }, + }); + + await new Promise((resolve) => passthrough.write(input, resolve)); + + const json = JSON.stringify(results, null, 2); + await fs.writeFile(`./tests/telegrams/dlms/${file}.json`, json); + parser.destroy(); + } +} -const encryptedAad = encryptFrame({ - frame: input, - key: TEST_DECRYPTION_KEY, - aad: TEST_AAD, -}); +// Encrypted DLMS frames +{ + const dlmsFileToEncrypt = 'aidon-example-2'; + console.log(`Using ${dlmsFileToEncrypt} as test case for encrypted DLMS telegrams`); + + const input = await readHexFile(`./tests/telegrams/dlms/${dlmsFileToEncrypt}.txt`); -const hexStringAad = bufferToHexString(encryptedAad); + const hdlcHeader = decodeHdlcHeader(input); -await fs.writeFile( - `./tests/telegrams/encrypted/${fileToEncrypt}-with-aad.txt`, - hexStringAad, - 'utf8', -); + const frame = input.subarray(0, hdlcHeader.frameLength + 2); + const frameContent = frame.subarray(hdlcHeader.consumedBytes) -const encryptedWithoutAad = encryptFrame({ - frame: input, - key: TEST_DECRYPTION_KEY, -}); + const llc = decodeLlcHeader(frameContent); + const content = frame.subarray( + hdlcHeader.consumedBytes + llc.consumedBytes, + frame.length - HDLC_FOOTER_LENGTH, + ); + + const encryptedAad = encryptFrame({ + frame: content.toString('binary'), + key: TEST_DECRYPTION_KEY, + aad: TEST_AAD, + }); + + const encryptedWithoutAad = encryptFrame({ + frame: content.toString('binary'), + key: TEST_DECRYPTION_KEY, + }); -const hexStringWithoutAad = bufferToHexString(encryptedWithoutAad); + const frameWithAad = wrapHdlcFrame(encryptedAad); + const frameWithoutAad = wrapHdlcFrame(encryptedWithoutAad); -await fs.writeFile( - `./tests/telegrams/encrypted/${fileToEncrypt}-without-aad.txt`, - hexStringWithoutAad, - 'utf8', -); + await writeHexFile( + `./tests/telegrams/dlms/encrypted/${dlmsFileToEncrypt}-with-aad.txt`, + frameWithAad, + ); + await writeHexFile( + `./tests/telegrams/dlms/encrypted/${dlmsFileToEncrypt}-without-aad.txt`, + frameWithoutAad, + ); +} \ No newline at end of file diff --git a/tools/wrap-hdlc.ts b/tools/wrap-hdlc.ts new file mode 100644 index 0000000..97fba8a --- /dev/null +++ b/tools/wrap-hdlc.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-console */ +/** Wraps a hex-encoded binary file in a HDLC frame. */ +import path from 'node:path'; +import { readHexFile, wrapHdlcFrame, writeHexFile } from '../tests/test-utils.js'; + +const srcPath = process.argv[2]; +const dstPath = process.argv[3]; + +if (srcPath === undefined || dstPath === undefined) { + console.error('Please provide a file path as argument.'); + console.log('Usage:'); + console.log('npm run tool:wrap-hdlc '); + process.exit(1); +} + +const srcPathResolved = path.resolve(process.cwd(), srcPath); +const dstPathResolved = path.resolve(process.cwd(), dstPath); + +const srcFile = await readHexFile(srcPathResolved); + +const hdlcFrame = wrapHdlcFrame(srcFile); + +const hexString = await writeHexFile(dstPathResolved, hdlcFrame); +console.log('File written to', dstPathResolved); +console.log('File size:', hdlcFrame.length); +console.log('File content:'); +console.log(hexString); diff --git a/tsconfig.json b/tsconfig.json index 846114c..f4c4023 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./node_modules/@tsconfig/node18/tsconfig.json", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "tools/*.ts"], "compilerOptions": { "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */, From 478a8d859fa8d39f3ec8b09973ed82b3a024fac6 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Wed, 21 May 2025 15:31:58 +0200 Subject: [PATCH 02/18] feat(dlms): update test telegrams --- src/protocols/cosem.ts | 4 +- src/protocols/dsmr.ts | 12 +- tests/stream/stream-detect-type.spec.ts | 4 +- .../dlms/aidon-example-2-encrypted.json | 9 + .../dlms/aidon-example-2-encrypted.txt | 41 ++++ tests/telegrams/dlms/kamstrup-example-1.json | 6 +- tests/telegrams/dlms/kamstrup-example-2.json | 6 +- tests/telegrams/dlms/kamstrup-example-3.json | 8 +- tests/telegrams/dsmr/dsmr-2.2-kfm-1.json | 32 +-- .../telegrams/dsmr/dsmr-3.0-spec-example.json | 34 ++-- tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 44 ++-- tests/telegrams/dsmr/dsmr-4.0-isk-2.json | 38 ++-- .../telegrams/dsmr/dsmr-4.0-spec-example.json | 44 ++-- tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 38 ++-- tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 38 ++-- .../dsmr/dsmr-4.2.2-spec-example.json | 60 +++--- tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 40 ++-- tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 38 ++-- tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 40 ++-- tests/telegrams/dsmr/dsmr-5.0-est-units.json | 50 +++-- tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 38 ++-- .../dsmr/dsmr-5.0-spec-example-lowercase.json | 38 ++-- .../telegrams/dsmr/dsmr-5.0-spec-example.json | 38 ++-- .../dsmr/dsmr-luxembourgh-spec-example.json | 48 ++--- .../dsmr/emucs-p1-v2.1.1-spec-example-1.json | 34 ++-- .../dsmr/emucs-p1-v2.1.1-spec-example-2.json | 34 ++-- ...dsmr-luxembourgh-spec-example-with-aad.txt | 190 +++++++++--------- ...r-luxembourgh-spec-example-without-aad.txt | 190 +++++++++--------- ...iskra-mt-382-no-crc-with-text-message.json | 32 +-- tests/telegrams/dsmr/iskra-mt-382-no-crc.json | 32 +-- .../kamstrup-OMNIA-e-meter-three-phase.json | 38 ++-- tests/telegrams/dsmr/sagemcom-xt211.json | 40 ++-- tests/telegrams/dsmr/unknown-xmx-1.json | 20 +- tools/update-test-telegrams.ts | 1 - 34 files changed, 768 insertions(+), 591 deletions(-) create mode 100644 tests/telegrams/dlms/aidon-example-2-encrypted.json create mode 100644 tests/telegrams/dlms/aidon-example-2-encrypted.txt diff --git a/src/protocols/cosem.ts b/src/protocols/cosem.ts index a4a9071..4a1642b 100644 --- a/src/protocols/cosem.ts +++ b/src/protocols/cosem.ts @@ -125,7 +125,7 @@ export const CosemLibrary = new CosemLibraryInternal() .addStringParser('0-0:1.0.0', ({ valueString, result }) => { result.metadata.timestamp = valueString; // TODO: Parse to date object }) - .addStringParser('*-*:96.1.1', ({ valueString, result }) => { + .addStringParser('0-0:96.1.1', ({ valueString, result }) => { result.metadata.equipmentId = valueString; }) .addNumberParser('1-*:1.8.*', ({ valueNumber, unit, obisCode, result }) => { @@ -215,7 +215,7 @@ export const CosemLibrary = new CosemLibraryInternal() result.metadata.events.voltageSwells = result.metadata.events.voltageSwells ?? {}; result.metadata.events.voltageSwells.l3 = valueNumber; }) - .addNumberParser('0-0:96.13.0', ({ valueString, result }) => { + .addStringParser('0-0:96.13.0', ({ valueString, result }) => { result.metadata.textMessage = valueString; }) .addNumberParser('0-0:96.13.1', ({ valueNumber, result }) => { diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts index 796eae9..7809fc9 100644 --- a/src/protocols/dsmr.ts +++ b/src/protocols/dsmr.ts @@ -1,7 +1,7 @@ import { decryptDlmsFrame } from './encryption.js'; import { SmartMeterParserError } from '../util/errors.js'; import { CosemLibrary } from './cosem.js'; -import { parseObisCodeFromString } from './obis-code.js'; +import { obisCodeToString, parseObisCodeFromString } from './obis-code.js'; import { calculateCrc16Arc } from '../util/crc.js'; import { BaseParserResult } from '../util/base-result.js'; @@ -44,7 +44,7 @@ export type DsmrParserResult = BaseParserResult & { /** Parses a string like "(1234.56*unit)", "(1234.56)", "(1234)" or "()". */ const NumberTypeRegex = /^\(([\d.]+)?(\*\w+)?\)/; /** Parses a string like "(string)". */ -const StringTypeRegex = /^\(([^)]+)?\)/; +const StringTypeRegex = /^\(([^)]*)?\)/; export const DSMR_SOF = 0x2f; // '/' export const CR = 0x0d; // '\r' @@ -70,8 +70,8 @@ export const isDsmrCrcValid = ({ crc: number; }) => { // Strip the CRC from the telegram - const telegramParts = telegram.split(CRLF); - const strippedTelegram = telegramParts[0] + CRLF; + const telegramParts = telegram.split(`${CRLF}!`); + const strippedTelegram = telegramParts[0] + CRLF + '!'; const calculatedCrc = calculateCrc16Arc(Buffer.from(strippedTelegram, DEFAULT_FRAME_ENCODING)); @@ -134,10 +134,10 @@ const decodeDsmrCosemLine = ({ const valueString = regexResult[1] ?? ''; const unit = regexResult[2] ? regexResult[2].slice(1) : null; - const valueNumber = parseFloat(valueString); + let valueNumber = parseFloat(valueString); if (isNaN(valueNumber)) { - return false; + valueNumber = 0; } parser.callback({ diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts index 2741d6f..bef513e 100644 --- a/tests/stream/stream-detect-type.spec.ts +++ b/tests/stream/stream-detect-type.spec.ts @@ -148,7 +148,7 @@ describe('Stream: Detect Type', () => { }); it('Detects encrypted DLMS telegrams', async () => { - const input = await readHexFile('./tests/telegrams/dlms/radiusel-example.txt'); + const input = await readHexFile('./tests/telegrams/dlms/aidon-example-2-encrypted.txt'); const stream = new PassThrough(); const callback = mock.fn(); @@ -167,7 +167,7 @@ describe('Stream: Detect Type', () => { }); it('Detects encrypted DLMS telegrams (chunks)', async () => { - const input = await readHexFile('./tests/telegrams/dlms/radiusel-example.txt'); + const input = await readHexFile('./tests/telegrams/dlms/aidon-example-2-encrypted.txt'); const stream = new PassThrough(); const callback = mock.fn(); diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.json b/tests/telegrams/dlms/aidon-example-2-encrypted.json new file mode 100644 index 0000000..decc1e8 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2-encrypted.json @@ -0,0 +1,9 @@ +[ + { + "error": { + "message": "Encrypted frame detected", + "name": "DecryptionRequired", + "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:58:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:124:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" + } + } +] \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.txt b/tests/telegrams/dlms/aidon-example-2-encrypted.txt new file mode 100644 index 0000000..ebb2d9c --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2-encrypted.txt @@ -0,0 +1,41 @@ +7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 +74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b +02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d +78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 +9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 +7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 +7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 +01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f +d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 +70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b +e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 +e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 +58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 +41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 +a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 +c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f +9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe +e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 +4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 +bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 +d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d +41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 +38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec +1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f +cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 +ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 +f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 +20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc +32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 +b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 +bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 +c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c +cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 +f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e +a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 +9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 +4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 +bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 +7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 +9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d 09 ee +97 c8 31 de 81 eb dd c2 ca cb dc 76 7e diff --git a/tests/telegrams/dlms/kamstrup-example-1.json b/tests/telegrams/dlms/kamstrup-example-1.json index 7f30a25..7b66d5f 100644 --- a/tests/telegrams/dlms/kamstrup-example-1.json +++ b/tests/telegrams/dlms/kamstrup-example-1.json @@ -24,11 +24,11 @@ "cosem": { "unknownObjects": [ "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)", "1-1:3.7.0(0)", "1-1:4.7.0(0)" ], "knownObjects": [ - "1-1:96.1.1(000000000000000000)", "1-1:1.7.0(0)", "1-1:2.7.0(0)", "1-1:31.7.0(0)", @@ -54,8 +54,6 @@ } }, "mBus": {}, - "metadata": { - "equipmentId": "000000000000000000" - } + "metadata": {} } ] \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-2.json b/tests/telegrams/dlms/kamstrup-example-2.json index 1fca444..6daea88 100644 --- a/tests/telegrams/dlms/kamstrup-example-2.json +++ b/tests/telegrams/dlms/kamstrup-example-2.json @@ -26,13 +26,13 @@ "cosem": { "unknownObjects": [ "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)", "1-1:3.7.0(0)", "1-1:4.7.0(0)", "1-1:3.8.0(0)", "1-1:4.8.0(0)" ], "knownObjects": [ - "1-1:96.1.1(000000000000000000)", "1-1:1.7.0(0)", "1-1:2.7.0(0)", "1-1:31.7.0(0)", @@ -64,8 +64,6 @@ } }, "mBus": {}, - "metadata": { - "equipmentId": "000000000000000000" - } + "metadata": {} } ] \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-3.json b/tests/telegrams/dlms/kamstrup-example-3.json index 8af5852..1a66104 100644 --- a/tests/telegrams/dlms/kamstrup-example-3.json +++ b/tests/telegrams/dlms/kamstrup-example-3.json @@ -25,10 +25,10 @@ }, "cosem": { "unknownObjects": [ - "1-1:0.0.5(5706567000000000)" + "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)" ], "knownObjects": [ - "1-1:96.1.1(000000000000000000)", "1-1:1.7.0(0)", "1-1:31.7.0(0)", "1-1:32.7.0(0)", @@ -48,8 +48,6 @@ } }, "mBus": {}, - "metadata": { - "equipmentId": "000000000000000000" - } + "metadata": {} } ] \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json index 7aa9164..e259d01 100644 --- a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json @@ -1,30 +1,36 @@ { - "raw": "/KMP5 ZABF001587315111\r\n0-0:96.1.1(205C4D246333034353537383234323121)\r\n1-0:1.8.1(00185.000*kWh)\r\n1-0:1.8.2(00084.000*kWh)\r\n1-0:2.8.1(00013.000*kWh)\r\n1-0:2.8.2(00019.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.98*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:17.0.0(999*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(3238313031453631373038389930337131)\r\n0-1:24.3.0(120517020000)(08)(60)(1)(0-1:24.2.1)(m3)\r\n(00124.477)\r\n0-1:24.4.0(1)\r\n!", - "header": { - "identifier": " ZABF001587315111", - "xxx": "KMP", - "z": "5" - }, - "metadata": { - "equipmentId": "205C4D246333034353537383234323121", + "dsmr": { + "raw": "/KMP5 ZABF001587315111\r\n0-0:96.1.1(205C4D246333034353537383234323121)\r\n1-0:1.8.1(00185.000*kWh)\r\n1-0:1.8.2(00084.000*kWh)\r\n1-0:2.8.1(00013.000*kWh)\r\n1-0:2.8.2(00019.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.98*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:17.0.0(999*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(3238313031453631373038389930337131)\r\n0-1:24.3.0(120517020000)(08)(60)(1)(0-1:24.2.1)(m3)\r\n(00124.477)\r\n0-1:24.4.0(1)\r\n!", + "header": { + "identifier": " ZABF001587315111", + "xxx": "KMP", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(999*A)", "0-0:96.3.10(1)", "(00124.477)", "0-1:24.4.0(1)" - ], + ] + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "equipmentId": "205C4D246333034353537383234323121", "numericMessage": 0, "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 185, - "returned": 13 + "received": 185000, + "returned": 13000 }, "2": { - "received": 84, - "returned": 19 + "received": 84000, + "returned": 19000 } }, "currentTariff": 1, diff --git a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json index 8cf82b6..c6873f6 100644 --- a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json @@ -1,30 +1,36 @@ { - "raw": "/ISk5\\2MT382-1000\r\n\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(12345.678*kWh)\r\n1-0:1.8.2(12345.678*kWh)\r\n1-0:2.8.1(12345.678*kWh)\r\n1-0:2.8.2(12345.678*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(001.19*kW)\r\n1-0:2.7.0(000.00*kW)\r\n0-0:17.0.0(016*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1(303132333435363738)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.1.0(03)\r\n0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(00000.000)\r\n0-1:24.4.0(1)\r\n!\r\n", - "header": { - "identifier": "\\2MT382-1000", - "xxx": "ISk", - "z": "5" - }, - "metadata": { - "equipmentId": "4B384547303034303436333935353037", + "dsmr": { + "raw": "/ISk5\\2MT382-1000\r\n\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(12345.678*kWh)\r\n1-0:1.8.2(12345.678*kWh)\r\n1-0:2.8.1(12345.678*kWh)\r\n1-0:2.8.2(12345.678*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(001.19*kW)\r\n1-0:2.7.0(000.00*kW)\r\n0-0:17.0.0(016*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1(303132333435363738)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.1.0(03)\r\n0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(00000.000)\r\n0-1:24.4.0(1)\r\n!\r\n", + "header": { + "identifier": "\\2MT382-1000", + "xxx": "ISk", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(016*A)", "0-0:96.3.10(1)", - "0-0:96.13.1(303132333435363738)", "(00000.000)", "0-1:24.4.0(1)" - ], + ] + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "equipmentId": "4B384547303034303436333935353037", + "numericMessage": 303132333435363700, "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 12345.678, - "returned": 12345.678 + "received": 12345678, + "returned": 12345678 }, "2": { - "received": 12345.678, - "returned": 12345.678 + "received": 12345678, + "returned": 12345678 } }, "currentTariff": 2, diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index 5162ad5..9b03327 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -1,22 +1,31 @@ { - "raw": "/ISk5\\2MT382-1 000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B", - "header": { - "identifier": "\\2MT382-1 000", - "xxx": "ISk", - "z": "5" - }, - "metadata": { - "dsmrVersion": 4, - "timestamp": "101209113020W", - "equipmentId": "4B384547303034303436333935353037", + "dsmr": { + "raw": "/ISk5\\2MT382-1 000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B", + "header": { + "identifier": "\\2MT382-1 000", + "xxx": "ISk", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(016.1*kW)", "0-0:96.3.10(1)", "1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)", "1-0:72:32.0(00000)", - "0-0:96.13.1(3031203631203831)", "0-1:24.4.0(1)" ], + "crc": { + "value": 21035, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "dsmrVersion": 4, + "timestamp": "101209113020W", + "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, "longPowerFailures": 2, @@ -30,17 +39,18 @@ "l3": 0 } }, + "numericMessage": 3031203631203831, "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 }, "2": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 } }, "currentTariff": 2, @@ -55,9 +65,5 @@ "value": 12785.123, "unit": "m3" } - }, - "crc": { - "value": 21035, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json index 0002007..34edcb0 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json @@ -1,19 +1,29 @@ { - "raw": "/XMX5LGBBFFB231216240\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(000101010000W)\r\n0-0:96.1.1(4530303034303031353931303932323134)\r\n1-0:1.8.1(001990.002*kWh)\r\n1-0:1.8.2(000000.000*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(999.9*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00023)\r\n0-0:96.7.9(00000)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!4F82", - "header": { - "identifier": "LGBBFFB231216240", - "xxx": "XMX", - "z": "5" - }, - "metadata": { - "dsmrVersion": 4, - "timestamp": "000101010000W", - "equipmentId": "4530303034303031353931303932323134", + "dsmr": { + "raw": "/XMX5LGBBFFB231216240\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(000101010000W)\r\n0-0:96.1.1(4530303034303031353931303932323134)\r\n1-0:1.8.1(001990.002*kWh)\r\n1-0:1.8.2(000000.000*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(999.9*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00023)\r\n0-0:96.7.9(00000)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!4F82", + "header": { + "identifier": "LGBBFFB231216240", + "xxx": "XMX", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(999.9*kW)", "0-0:96.3.10(1)", "1-0:99.97.0(0)(0-0:96.7.19)" ], + "crc": { + "value": 20354, + "valid": true + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "dsmrVersion": 4, + "timestamp": "000101010000W", + "equipmentId": "4530303034303031353931303932323134", "events": { "powerFailures": 23, "longPowerFailures": 0, @@ -30,7 +40,7 @@ "electricity": { "tariffs": { "1": { - "received": 1990.002, + "received": 1990002, "returned": 0 }, "2": { @@ -42,9 +52,5 @@ "powerReceivedTotal": 0, "powerReturnedTotal": 0 }, - "mBus": {}, - "crc": { - "value": 20354, - "valid": true - } + "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index e24395b..ddc13eb 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -1,22 +1,31 @@ { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B\r\n", - "header": { - "identifier": "\\2MT382-1000", - "xxx": "ISk", - "z": "5" - }, - "metadata": { - "dsmrVersion": 4, - "timestamp": "101209113020W", - "equipmentId": "4B384547303034303436333935353037", + "dsmr": { + "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B\r\n", + "header": { + "identifier": "\\2MT382-1000", + "xxx": "ISk", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(016.1*kW)", "0-0:96.3.10(1)", "1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)", "1-0:72:32.0(00000)", - "0-0:96.13.1(3031203631203831)", "0-1:24.4.0(1)" ], + "crc": { + "value": 21035, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "dsmrVersion": 4, + "timestamp": "101209113020W", + "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, "longPowerFailures": 2, @@ -30,17 +39,18 @@ "l3": 0 } }, + "numericMessage": 3031203631203831, "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 }, "2": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 } }, "currentTariff": 2, @@ -55,9 +65,5 @@ "value": 12785.123, "unit": "m3" } - }, - "crc": { - "value": 21035, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index d4f92b5..6e81932 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -1,9 +1,22 @@ { - "raw": "/KFM5KAIFA-METER\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(180306123056W)\r\n0-0:96.1.1(4530303033303030303032313234383133)\r\n1-0:1.8.1(004726.494*kWh)\r\n1-0:1.8.2(004844.281*kWh)\r\n1-0:2.8.1(003284.320*kWh)\r\n1-0:2.8.2(007764.691*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(01.869*kW)\r\n0-0:96.7.21(00013)\r\n0-0:96.7.9(00007)\r\n1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)\r\n1-0:32.32.0(00000)\r\n1-0:52.32.0(00000)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(003*A)\r\n1-0:71.7.0(002*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.688*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:42.7.0(00.778*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(00.403*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303136353631323033353830313133)\r\n0-1:24.2.1(180306120000W)(05359.919*m3)\r\n!A737", - "header": { - "identifier": "KAIFA-METER", - "xxx": "KFM", - "z": "5" + "dsmr": { + "raw": "/KFM5KAIFA-METER\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(180306123056W)\r\n0-0:96.1.1(4530303033303030303032313234383133)\r\n1-0:1.8.1(004726.494*kWh)\r\n1-0:1.8.2(004844.281*kWh)\r\n1-0:2.8.1(003284.320*kWh)\r\n1-0:2.8.2(007764.691*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(01.869*kW)\r\n0-0:96.7.21(00013)\r\n0-0:96.7.9(00007)\r\n1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)\r\n1-0:32.32.0(00000)\r\n1-0:52.32.0(00000)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(003*A)\r\n1-0:71.7.0(002*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.688*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:42.7.0(00.778*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(00.403*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303136353631323033353830313133)\r\n0-1:24.2.1(180306120000W)(05359.919*m3)\r\n!A737", + "header": { + "identifier": "KAIFA-METER", + "xxx": "KFM", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)" + ], + "crc": { + "value": 42807, + "valid": true + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 4.2, @@ -23,21 +36,18 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)" - ], "numericMessage": 0, "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 4726.494, - "returned": 3284.32 + "received": 4726494, + "returned": 3284320 }, "2": { - "received": 4844.281, - "returned": 7764.691 + "received": 4844281, + "returned": 7764691 } }, "currentTariff": 2, @@ -67,9 +77,5 @@ "value": 5359.919, "unit": "m3" } - }, - "crc": { - "value": 42807, - "valid": true } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index 056cbdc..eb9158b 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -1,9 +1,22 @@ { - "raw": "/XMX5LGBBFG10\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(170108161107W)\r\n0-0:96.1.1(4530303331303033303031363939353135)\r\n1-0:1.8.1(002074.842*kWh)\r\n1-0:1.8.2(000881.383*kWh)\r\n1-0:2.8.1(000010.981*kWh)\r\n1-0:2.8.2(000028.031*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.494*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:21.7.0(00.494*kW)\r\n1-0:22.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303139333430323231313938343135)\r\n0-1:24.2.1(170108160000W)(01234.000*m3)\r\n!D3B0", - "header": { - "identifier": "LGBBFG10", - "xxx": "XMX", - "z": "5" + "dsmr": { + "raw": "/XMX5LGBBFG10\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(170108161107W)\r\n0-0:96.1.1(4530303331303033303031363939353135)\r\n1-0:1.8.1(002074.842*kWh)\r\n1-0:1.8.2(000881.383*kWh)\r\n1-0:2.8.1(000010.981*kWh)\r\n1-0:2.8.2(000028.031*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.494*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:21.7.0(00.494*kW)\r\n1-0:22.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303139333430323231313938343135)\r\n0-1:24.2.1(170108160000W)(01234.000*m3)\r\n!D3B0", + "header": { + "identifier": "LGBBFG10", + "xxx": "XMX", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)" + ], + "crc": { + "value": 54192, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 4.2, @@ -19,21 +32,18 @@ "l1": 0 } }, - "unknownLines": [ - "1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)" - ], "numericMessage": 0, "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 2074.842, - "returned": 10.981 + "received": 2074842, + "returned": 10981 }, "2": { - "received": 881.383, - "returned": 28.031 + "received": 881383, + "returned": 28031 } }, "currentTariff": 1, @@ -57,9 +67,5 @@ "value": 1234, "unit": "m3" } - }, - "crc": { - "value": 54192, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index d67813b..a63a0f5 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -1,9 +1,32 @@ { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:31.7.0.255(001*A)\r\n1-0:51.7.0.255(002*A)\r\n1-0:71.7.0.255(003*A)\r\n1-0:21.7.0.255(01.111*kW)\r\n1-0:41.7.0.255(02.222*kW)\r\n1-0:61.7.0.255(03.333*kW)\r\n1-0:22.7.0.255(04.444*kW)\r\n1-0:42.7.0.255(05.555*kW)\r\n1-0:62.7.0.255(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n!CE7C", - "header": { - "identifier": "\\2MT382-1000", - "xxx": "ISk", - "z": "5" + "dsmr": { + "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:31.7.0.255(001*A)\r\n1-0:51.7.0.255(002*A)\r\n1-0:71.7.0.255(003*A)\r\n1-0:21.7.0.255(01.111*kW)\r\n1-0:41.7.0.255(02.222*kW)\r\n1-0:61.7.0.255(03.333*kW)\r\n1-0:22.7.0.255(04.444*kW)\r\n1-0:42.7.0.255(05.555*kW)\r\n1-0:62.7.0.255(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n!CE7C", + "header": { + "identifier": "\\2MT382-1000", + "xxx": "ISk", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)", + "1-0:72:32.0(00000)", + "1-0:31.7.0.255(001*A)", + "1-0:51.7.0.255(002*A)", + "1-0:71.7.0.255(003*A)", + "1-0:21.7.0.255(01.111*kW)", + "1-0:41.7.0.255(02.222*kW)", + "1-0:61.7.0.255(03.333*kW)", + "1-0:22.7.0.255(04.444*kW)", + "1-0:42.7.0.255(05.555*kW)", + "1-0:62.7.0.255(06.666*kW)" + ], + "crc": { + "value": 52860, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 4.2, @@ -22,31 +45,18 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)", - "1-0:72:32.0(00000)", - "0-0:96.13.1(3031203631203831)", - "1-0:31.7.0.255(001*A)", - "1-0:51.7.0.255(002*A)", - "1-0:71.7.0.255(003*A)", - "1-0:21.7.0.255(01.111*kW)", - "1-0:41.7.0.255(02.222*kW)", - "1-0:61.7.0.255(03.333*kW)", - "1-0:22.7.0.255(04.444*kW)", - "1-0:42.7.0.255(05.555*kW)", - "1-0:62.7.0.255(06.666*kW)" - ], + "numericMessage": 3031203631203831, "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 }, "2": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 } }, "currentTariff": 2, @@ -61,9 +71,5 @@ "value": 12785.123, "unit": "m3" } - }, - "crc": { - "value": 52860, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index d5cc078..82e2ce9 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -1,9 +1,22 @@ { - "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(210330192305S)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000009.533*kWh)\r\n1-0:1.8.2(000014.154*kWh)\r\n1-0:2.8.1(002248.911*kWh)\r\n1-0:2.8.2(005072.177*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.566*kW)\r\n0-0:96.7.21(00220)\r\n0-0:96.7.9(00034)\r\n1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)\r\n1-0:32.32.0(00024)\r\n1-0:52.32.0(00024)\r\n1-0:72.32.0(00024)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(227.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(004*A)\r\n1-0:51.7.0(004*A)\r\n1-0:71.7.0(004*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.068*kW)\r\n1-0:42.7.0(00.240*kW)\r\n1-0:62.7.0(00.257*kW)\r\n!D59D", - "header": { - "identifier": "\\T210-D ESMR5.0", - "xxx": "Ene", - "z": "5" + "dsmr": { + "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(210330192305S)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000009.533*kWh)\r\n1-0:1.8.2(000014.154*kWh)\r\n1-0:2.8.1(002248.911*kWh)\r\n1-0:2.8.2(005072.177*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.566*kW)\r\n0-0:96.7.21(00220)\r\n0-0:96.7.9(00034)\r\n1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)\r\n1-0:32.32.0(00024)\r\n1-0:52.32.0(00024)\r\n1-0:72.32.0(00024)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(227.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(004*A)\r\n1-0:51.7.0(004*A)\r\n1-0:71.7.0(004*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.068*kW)\r\n1-0:42.7.0(00.240*kW)\r\n1-0:62.7.0(00.257*kW)\r\n!D59D", + "header": { + "identifier": "\\T210-D ESMR5.0", + "xxx": "Ene", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)" + ], + "crc": { + "value": 54685, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,20 +36,17 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)" - ], "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 9.533, - "returned": 2248.911 + "received": 9533, + "returned": 2248911 }, "2": { - "received": 14.154, - "returned": 5072.177 + "received": 14154, + "returned": 5072177 } }, "currentTariff": 2, @@ -63,9 +73,5 @@ "l3": 0.257 } }, - "mBus": {}, - "crc": { - "value": 54685, - "valid": false - } + "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index ff7e3c6..999df63 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -1,9 +1,22 @@ { - "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(180108202537W)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000000.855*kWh)\r\n1-0:1.8.2(000000.693*kWh)\r\n1-0:2.8.1(000000.084*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.134*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00008)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(229.0*V)\r\n1-0:52.7.0(226.0*V)\r\n1-0:72.7.0(229.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.094*kW)\r\n1-0:41.7.0(00.040*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(serienummer)\r\n0-1:24.2.1(180108205500W)(00001.290*m3)\r\n!B055", - "header": { - "identifier": "\\T210-D ESMR5.0", - "xxx": "Ene", - "z": "5" + "dsmr": { + "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(180108202537W)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000000.855*kWh)\r\n1-0:1.8.2(000000.693*kWh)\r\n1-0:2.8.1(000000.084*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.134*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00008)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(229.0*V)\r\n1-0:52.7.0(226.0*V)\r\n1-0:72.7.0(229.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.094*kW)\r\n1-0:41.7.0(00.040*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(serienummer)\r\n0-1:24.2.1(180108205500W)(00001.290*m3)\r\n!B055", + "header": { + "identifier": "\\T210-D ESMR5.0", + "xxx": "Ene", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)" + ], + "crc": { + "value": 45141, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,20 +36,16 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)", - "0-1:96.1.0(serienummer)" - ], "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 0.855, - "returned": 0.084 + "received": 855, + "returned": 84 }, "2": { - "received": 0.693, + "received": 693, "returned": 0 } }, @@ -67,13 +76,10 @@ "mBus": { "1": { "deviceType": 3, + "equipmentId": "serienummer", "timestamp": "180108205500W", "value": 1.29, "unit": "m3" } - }, - "crc": { - "value": 45141, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index 9ef7a78..d46c005 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -1,9 +1,23 @@ { - "raw": "/Ene5\\T211 ESMR 5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(240220170958W)\r\n0-0:96.1.1(4530303632303030303134353236323233)\r\n1-0:1.8.1(000565.971*kWh)\r\n1-0:1.8.2(000694.269*kWh)\r\n1-0:2.8.1(000006.754*kWh)\r\n1-0:2.8.2(000007.849*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.723*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00010)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00001)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(225.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.654*kW)\r\n1-0:41.7.0(00.069*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:42.1.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303732303033393634343938373139)\r\n0-1:24.2.1(240220171000W)(06362.120*m3)\r\n!B788", - "header": { - "identifier": "\\T211 ESMR 5.0", - "xxx": "Ene", - "z": "5" + "dsmr": { + "raw": "/Ene5\\T211 ESMR 5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(240220170958W)\r\n0-0:96.1.1(4530303632303030303134353236323233)\r\n1-0:1.8.1(000565.971*kWh)\r\n1-0:1.8.2(000694.269*kWh)\r\n1-0:2.8.1(000006.754*kWh)\r\n1-0:2.8.2(000007.849*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.723*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00010)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00001)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(225.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.654*kW)\r\n1-0:41.7.0(00.069*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:42.1.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303732303033393634343938373139)\r\n0-1:24.2.1(240220171000W)(06362.120*m3)\r\n!B788", + "header": { + "identifier": "\\T211 ESMR 5.0", + "xxx": "Ene", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(0)(0-0:96.7.19)", + "1-0:42.1.0(00.000*kW)" + ], + "crc": { + "value": 46984, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,21 +37,17 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(0)(0-0:96.7.19)", - "1-0:42.1.0(00.000*kW)" - ], "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 565.971, - "returned": 6.754 + "received": 565971, + "returned": 6754 }, "2": { - "received": 694.269, - "returned": 7.849 + "received": 694269, + "returned": 7849 } }, "currentTariff": 2, @@ -71,9 +81,5 @@ "value": 6362.12, "unit": "m3" } - }, - "crc": { - "value": 46984, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index 688cf25..e9fd376 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -1,13 +1,11 @@ { - "raw": "/EST5\\123456789_A\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(250319102812W)\r\n1-0:1.8.0(000201282*Wh)\r\n1-0:1.8.1(000134904*Wh)\r\n1-0:1.8.2(000066378*Wh)\r\n1-0:1.7.0(000000000*W)\r\n1-0:2.8.0(000320908*Wh)\r\n1-0:2.8.1(000317131*Wh)\r\n1-0:2.8.2(000003777*Wh)\r\n1-0:2.7.0(000003996*W)\r\n1-0:3.8.0(000036520*varh)\r\n1-0:3.8.1(000032377*varh)\r\n1-0:3.8.2(000004143*varh)\r\n1-0:3.7.0(000000000*var)\r\n1-0:4.8.0(000259742*varh)\r\n1-0:4.8.1(000169558*varh)\r\n1-0:4.8.2(000090184*varh)\r\n1-0:4.7.0(000000221*var)\r\n!FFFF\r\n", - "header": { - "identifier": "\\123456789_A", - "xxx": "EST", - "z": "5" - }, - "metadata": { - "dsmrVersion": 5, - "timestamp": "250319102812W", + "dsmr": { + "raw": "/EST5\\123456789_A\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(250319102812W)\r\n1-0:1.8.0(000201282*Wh)\r\n1-0:1.8.1(000134904*Wh)\r\n1-0:1.8.2(000066378*Wh)\r\n1-0:1.7.0(000000000*W)\r\n1-0:2.8.0(000320908*Wh)\r\n1-0:2.8.1(000317131*Wh)\r\n1-0:2.8.2(000003777*Wh)\r\n1-0:2.7.0(000003996*W)\r\n1-0:3.8.0(000036520*varh)\r\n1-0:3.8.1(000032377*varh)\r\n1-0:3.8.2(000004143*varh)\r\n1-0:3.7.0(000000000*var)\r\n1-0:4.8.0(000259742*varh)\r\n1-0:4.8.1(000169558*varh)\r\n1-0:4.8.2(000090184*varh)\r\n1-0:4.7.0(000000221*var)\r\n!FFFF\r\n", + "header": { + "identifier": "\\123456789_A", + "xxx": "EST", + "z": "5" + }, "unknownLines": [ "1-0:3.8.0(000036520*varh)", "1-0:3.8.1(000032377*varh)", @@ -17,29 +15,37 @@ "1-0:4.8.1(000169558*varh)", "1-0:4.8.2(000090184*varh)", "1-0:4.7.0(000000221*var)" - ] + ], + "crc": { + "value": 65535, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "dsmrVersion": 5, + "timestamp": "250319102812W" }, "electricity": { "total": { - "received": 201.282, - "returned": 320.908 + "received": 201282, + "returned": 320908 }, "tariffs": { "1": { - "received": 134.904, - "returned": 317.131 + "received": 134904, + "returned": 317131 }, "2": { - "received": 66.378, - "returned": 3.777 + "received": 66378, + "returned": 3777 } }, "powerReceivedTotal": 0, - "powerReturnedTotal": 3.996 + "powerReturnedTotal": 3996000 }, - "mBus": {}, - "crc": { - "value": 65535, - "valid": false - } + "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index 15d6f0a..6c21982 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -1,9 +1,22 @@ { - "raw": "/ISK5\\2M550T-1011\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(181106140429W)\r\n0-0:96.1.1(4530303334303036383130353136343136)\r\n1-0:1.8.1(003808.351*kWh)\r\n1-0:1.8.2(002948.827*kWh)\r\n1-0:2.8.1(001285.951*kWh)\r\n1-0:2.8.2(002876.514*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.498*kW)\r\n0-0:96.7.21(00006)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00002)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00001)\r\n1-0:52.36.0(00001)\r\n1-0:72.36.0(00001)\r\n0-0:96.13.0()\r\n1-0:32.7.0(236.0*V)\r\n1-0:52.7.0(232.6*V)\r\n1-0:72.7.0(235.1*V)\r\n1-0:31.7.0(002*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.033*kW)\r\n1-0:61.7.0(00.132*kW)\r\n1-0:22.7.0(00.676*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303339303031373030343630313137)\r\n0-1:24.2.1(181106140010W)(01569.646*m3)\r\n!1F28", - "header": { - "identifier": "\\2M550T-1011", - "xxx": "ISK", - "z": "5" + "dsmr": { + "raw": "/ISK5\\2M550T-1011\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(181106140429W)\r\n0-0:96.1.1(4530303334303036383130353136343136)\r\n1-0:1.8.1(003808.351*kWh)\r\n1-0:1.8.2(002948.827*kWh)\r\n1-0:2.8.1(001285.951*kWh)\r\n1-0:2.8.2(002876.514*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.498*kW)\r\n0-0:96.7.21(00006)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00002)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00001)\r\n1-0:52.36.0(00001)\r\n1-0:72.36.0(00001)\r\n0-0:96.13.0()\r\n1-0:32.7.0(236.0*V)\r\n1-0:52.7.0(232.6*V)\r\n1-0:72.7.0(235.1*V)\r\n1-0:31.7.0(002*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.033*kW)\r\n1-0:61.7.0(00.132*kW)\r\n1-0:22.7.0(00.676*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303339303031373030343630313137)\r\n0-1:24.2.1(181106140010W)(01569.646*m3)\r\n!1F28", + "header": { + "identifier": "\\2M550T-1011", + "xxx": "ISK", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)" + ], + "crc": { + "value": 7976, + "valid": true + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,20 +36,17 @@ "l3": 1 } }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)" - ], "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 3808.351, - "returned": 1285.951 + "received": 3808351, + "returned": 1285951 }, "2": { - "received": 2948.827, - "returned": 2876.514 + "received": 2948827, + "returned": 2876514 } }, "currentTariff": 2, @@ -71,9 +81,5 @@ "value": 1569.646, "unit": "m3" } - }, - "crc": { - "value": 7976, - "valid": true } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index 30a67e5..d418957 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -1,9 +1,22 @@ { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kwh)\r\n1-0:1.8.2(123456.789*kwh)\r\n1-0:2.8.1(123456.789*kwh)\r\n1-0:2.8.2(123456.789*kwh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kw)\r\n1-0:2.7.0(00.000*kw)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*v)\r\n1-0:52.7.0(220.2*v)\r\n1-0:72.7.0(220.3*v)\r\n1-0:31.7.0(001*a)\r\n1-0:51.7.0(002*a)\r\n1-0:71.7.0(003*a)\r\n1-0:21.7.0(01.111*kw)\r\n1-0:41.7.0(02.222*kw)\r\n1-0:61.7.0(03.333*kw)\r\n1-0:22.7.0(04.444*kw)\r\n1-0:42.7.0(05.555*kw)\r\n1-0:62.7.0(06.666*kw)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", - "header": { - "identifier": "\\2MT382-1000", - "xxx": "ISk", - "z": "5" + "dsmr": { + "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kwh)\r\n1-0:1.8.2(123456.789*kwh)\r\n1-0:2.8.1(123456.789*kwh)\r\n1-0:2.8.2(123456.789*kwh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kw)\r\n1-0:2.7.0(00.000*kw)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*v)\r\n1-0:52.7.0(220.2*v)\r\n1-0:72.7.0(220.3*v)\r\n1-0:31.7.0(001*a)\r\n1-0:51.7.0(002*a)\r\n1-0:71.7.0(003*a)\r\n1-0:21.7.0(01.111*kw)\r\n1-0:41.7.0(02.222*kw)\r\n1-0:61.7.0(03.333*kw)\r\n1-0:22.7.0(04.444*kw)\r\n1-0:42.7.0(05.555*kw)\r\n1-0:62.7.0(06.666*kw)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", + "header": { + "identifier": "\\2MT382-1000", + "xxx": "ISk", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" + ], + "crc": { + "value": 61231, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,20 +36,17 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" - ], "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 }, "2": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 } }, "currentTariff": 2, @@ -71,9 +81,5 @@ "value": 12785.123, "unit": "m3" } - }, - "crc": { - "value": 61231, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index c507903..8cd282d 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -1,9 +1,22 @@ { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*V)\r\n1-0:52.7.0(220.2*V)\r\n1-0:72.7.0(220.3*V)\r\n1-0:31.7.0(001*A)\r\n1-0:51.7.0(002*A)\r\n1-0:71.7.0(003*A)\r\n1-0:21.7.0(01.111*kW)\r\n1-0:41.7.0(02.222*kW)\r\n1-0:61.7.0(03.333*kW)\r\n1-0:22.7.0(04.444*kW)\r\n1-0:42.7.0(05.555*kW)\r\n1-0:62.7.0(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", - "header": { - "identifier": "\\2MT382-1000", - "xxx": "ISk", - "z": "5" + "dsmr": { + "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*V)\r\n1-0:52.7.0(220.2*V)\r\n1-0:72.7.0(220.3*V)\r\n1-0:31.7.0(001*A)\r\n1-0:51.7.0(002*A)\r\n1-0:71.7.0(003*A)\r\n1-0:21.7.0(01.111*kW)\r\n1-0:41.7.0(02.222*kW)\r\n1-0:61.7.0(03.333*kW)\r\n1-0:22.7.0(04.444*kW)\r\n1-0:42.7.0(05.555*kW)\r\n1-0:62.7.0(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", + "header": { + "identifier": "\\2MT382-1000", + "xxx": "ISk", + "z": "5" + }, + "unknownLines": [ + "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" + ], + "crc": { + "value": 61231, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "dsmrVersion": 5, @@ -23,20 +36,17 @@ "l3": 0 } }, - "unknownLines": [ - "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" - ], "textMessage": "303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F" }, "electricity": { "tariffs": { "1": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 }, "2": { - "received": 123456.789, - "returned": 123456.789 + "received": 123456789, + "returned": 123456789 } }, "currentTariff": 2, @@ -71,9 +81,5 @@ "value": 12785.123, "unit": "m3" } - }, - "crc": { - "value": 61231, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index ba788f6..643deca 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -1,13 +1,11 @@ { - "raw": "/Lux5\\253694471_M\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(200706104157S)\r\n0-0:42.0.0(53414731303330373930303032353734)\r\n1-0:1.8.0(000025.653*kWh)\r\n1-0:2.8.0(000000.040*kWh)\r\n1-0:3.8.0(000000.835*kvarh)\r\n1-0:4.8.0(000063.781*kvarh)\r\n1-0:1.7.0(00.005*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:3.7.0(00.000*kvar)\r\n1-0:4.7.0(00.000*kvar)\r\n0-0:17.0.0(069.0*kVA)\r\n1-0:9.7.0(00.021*kVA)\r\n1-0:10.7.0(00.000*kVA)\r\n1-1:31.4.0(100*A)(-063*A)\r\n0-0:96.3.10(1)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-0:96.7.21(00099)\r\n1-0:32.32.0(00040)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n0-0:96.13.2()\r\n0-0:96.13.3()\r\n0-0:96.13.4()\r\n0-0:96.13.5()\r\n1-0:32.7.0(233.0*V)\r\n1-0:52.7.0(000.0*V)\r\n1-0:72.7.0(001.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.005*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:23.7.0(00.000*kvar)\r\n1-0:43.7.0(00.000*kvar)\r\n1-0:63.7.0(00.000*kvar)\r\n1-0:24.7.0(00.000*kvar)\r\n1-0:44.7.0(00.000*kvar)\r\n1-0:64.7.0(00.000*kvar)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(464C4F313839393030303630333535)\r\n0-1:24.2.1(200706103140S)(00000.006*m3)\r\n0-1:24.4.0(0)\r\n0-2:24.1.0(007)\r\n0-2:96.1.0()\r\n0-2:24.2.1(632525252525S)(00000.000)\r\n0-2:24.4.0(1)\r\n0-3:24.1.0(007)\r\n0-3:96.1.0()\r\n0-3:24.2.1(632525252525S)(00000.000)\r\n0-3:24.4.0(1)\r\n0-4:24.1.0(003)\r\n0-4:96.1.0(454C53333533353839393830333030)\r\n0-4:24.2.1(200706102900S)(00028.103*m3)\r\n0-4:24.4.0(1)\r\n!8B52", - "header": { - "identifier": "\\253694471_M", - "xxx": "Lux", - "z": "5" - }, - "metadata": { - "dsmrVersion": 4.2, - "timestamp": "200706104157S", + "dsmr": { + "raw": "/Lux5\\253694471_M\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(200706104157S)\r\n0-0:42.0.0(53414731303330373930303032353734)\r\n1-0:1.8.0(000025.653*kWh)\r\n1-0:2.8.0(000000.040*kWh)\r\n1-0:3.8.0(000000.835*kvarh)\r\n1-0:4.8.0(000063.781*kvarh)\r\n1-0:1.7.0(00.005*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:3.7.0(00.000*kvar)\r\n1-0:4.7.0(00.000*kvar)\r\n0-0:17.0.0(069.0*kVA)\r\n1-0:9.7.0(00.021*kVA)\r\n1-0:10.7.0(00.000*kVA)\r\n1-1:31.4.0(100*A)(-063*A)\r\n0-0:96.3.10(1)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-0:96.7.21(00099)\r\n1-0:32.32.0(00040)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n0-0:96.13.2()\r\n0-0:96.13.3()\r\n0-0:96.13.4()\r\n0-0:96.13.5()\r\n1-0:32.7.0(233.0*V)\r\n1-0:52.7.0(000.0*V)\r\n1-0:72.7.0(001.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.005*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:23.7.0(00.000*kvar)\r\n1-0:43.7.0(00.000*kvar)\r\n1-0:63.7.0(00.000*kvar)\r\n1-0:24.7.0(00.000*kvar)\r\n1-0:44.7.0(00.000*kvar)\r\n1-0:64.7.0(00.000*kvar)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(464C4F313839393030303630333535)\r\n0-1:24.2.1(200706103140S)(00000.006*m3)\r\n0-1:24.4.0(0)\r\n0-2:24.1.0(007)\r\n0-2:96.1.0()\r\n0-2:24.2.1(632525252525S)(00000.000)\r\n0-2:24.4.0(1)\r\n0-3:24.1.0(007)\r\n0-3:96.1.0()\r\n0-3:24.2.1(632525252525S)(00000.000)\r\n0-3:24.4.0(1)\r\n0-4:24.1.0(003)\r\n0-4:96.1.0(454C53333533353839393830333030)\r\n0-4:24.2.1(200706102900S)(00028.103*m3)\r\n0-4:24.4.0(1)\r\n!8B52", + "header": { + "identifier": "\\253694471_M", + "xxx": "Lux", + "z": "5" + }, "unknownLines": [ "0-0:42.0.0(53414731303330373930303032353734)", "1-0:3.8.0(000000.835*kvarh)", @@ -31,15 +29,23 @@ "1-0:24.7.0(00.000*kvar)", "1-0:44.7.0(00.000*kvar)", "1-0:64.7.0(00.000*kvar)", - "0-1:96.1.0(464C4F313839393030303630333535)", "0-1:24.4.0(0)", - "0-2:96.1.0()", "0-2:24.4.0(1)", - "0-3:96.1.0()", "0-3:24.4.0(1)", - "0-4:96.1.0(454C53333533353839393830333030)", "0-4:24.4.0(1)" ], + "crc": { + "value": 35666, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "dsmrVersion": 4.2, + "timestamp": "200706104157S", "events": { "powerFailures": 99, "voltageSags": { @@ -57,8 +63,8 @@ }, "electricity": { "total": { - "received": 25.653, - "returned": 0.04 + "received": 25653, + "returned": 40 }, "powerReceivedTotal": 0.005, "powerReturnedTotal": 0, @@ -86,29 +92,25 @@ "mBus": { "1": { "deviceType": 3, + "equipmentId": "464C4F313839393030303630333535", "timestamp": "200706103140S", "value": 0.006, "unit": "m3" }, "2": { "deviceType": 7, - "timestamp": "632525252525S", - "value": 0 + "equipmentId": "" }, "3": { "deviceType": 7, - "timestamp": "632525252525S", - "value": 0 + "equipmentId": "" }, "4": { "deviceType": 3, + "equipmentId": "454C53333533353839393830333030", "timestamp": "200706102900S", "value": 28.103, "unit": "m3" } - }, - "crc": { - "value": 35666, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json index caeab1c..1dc5961 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json @@ -1,11 +1,11 @@ { - "raw": "/FLU5\\253769484_A\r\n0-0:96.1.4(50221)\r\n1-0:94.32.1(400)\r\n0-0:96.1.1(3153414733313031303231363035)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512135409S)\r\n1-0:1.8.1(000000.034*kWh)\r\n1-0:1.8.2(000015.758*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.011*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:32.7.0(234.7*V)\r\n1-0:52.7.0(234.7*V)\r\n1-0:72.7.0(234.7*V)\r\n1-0:31.7.0(000.00*A)\r\n1-0:51.7.0(000.00*A)\r\n1-0:71.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", - "header": { - "identifier": "\\253769484_A", - "xxx": "FLU", - "z": "5" - }, - "metadata": { + "dsmr": { + "raw": "/FLU5\\253769484_A\r\n0-0:96.1.4(50221)\r\n1-0:94.32.1(400)\r\n0-0:96.1.1(3153414733313031303231363035)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512135409S)\r\n1-0:1.8.1(000000.034*kWh)\r\n1-0:1.8.2(000015.758*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.011*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:32.7.0(234.7*V)\r\n1-0:52.7.0(234.7*V)\r\n1-0:72.7.0(234.7*V)\r\n1-0:31.7.0(000.00*A)\r\n1-0:51.7.0(000.00*A)\r\n1-0:71.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", + "header": { + "identifier": "\\253769484_A", + "xxx": "FLU", + "z": "5" + }, "unknownLines": [ "0-0:96.1.4(50221)", "1-0:94.32.1(400)", @@ -26,6 +26,16 @@ "0-2:96.1.1(3853414731323334353637383930)", "0-2:96.1.2(353431343430303132333435363738393033)" ], + "crc": { + "value": 4660, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { "equipmentId": "3153414733313031303231363035", "timestamp": "200512135409S", "textMessage": "" @@ -33,12 +43,12 @@ "electricity": { "tariffs": { "1": { - "received": 0.034, + "received": 34, "returned": 0 }, "2": { - "received": 15.758, - "returned": 0.011 + "received": 15758, + "returned": 11 } }, "currentTariff": 1, @@ -78,9 +88,5 @@ "value": 872.234, "unit": "m3" } - }, - "crc": { - "value": 4660, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json index 8ba7d99..0e1899d 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json @@ -1,11 +1,11 @@ { - "raw": "/FLU5\\253770234_A\r\n0-0:96.1.4(50221)\r\n0-0:96.1.1(3153414731313030303030323331)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512145552S)\r\n1-0:1.8.1(000000.915*kWh)\r\n1-0:1.8.2(000001.955*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.030*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:32.7.0(234.6*V)\r\n1-0:31.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", - "header": { - "identifier": "\\253770234_A", - "xxx": "FLU", - "z": "5" - }, - "metadata": { + "dsmr": { + "raw": "/FLU5\\253770234_A\r\n0-0:96.1.4(50221)\r\n0-0:96.1.1(3153414731313030303030323331)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512145552S)\r\n1-0:1.8.1(000000.915*kWh)\r\n1-0:1.8.2(000001.955*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.030*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:32.7.0(234.6*V)\r\n1-0:31.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", + "header": { + "identifier": "\\253770234_A", + "xxx": "FLU", + "z": "5" + }, "unknownLines": [ "0-0:96.1.4(50221)", "0-0:96.1.2(353431343430303132333435363738393030)", @@ -25,6 +25,16 @@ "0-2:96.1.1(3853414731323334353637383930)", "0-2:96.1.2(353431343430303132333435363738393033)" ], + "crc": { + "value": 4660, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { "equipmentId": "3153414731313030303030323331", "timestamp": "200512145552S", "textMessage": "" @@ -32,12 +42,12 @@ "electricity": { "tariffs": { "1": { - "received": 0.915, + "received": 915, "returned": 0 }, "2": { - "received": 1.955, - "returned": 0.03 + "received": 1955, + "returned": 30 } }, "currentTariff": 1, @@ -69,9 +79,5 @@ "value": 872.234, "unit": "m3" } - }, - "crc": { - "value": 4660, - "valid": false } } \ No newline at end of file diff --git a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt index 43df5e3..97e7c95 100644 --- a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt +++ b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt @@ -1,95 +1,95 @@ -db087379737469746c658205de301122 -3344b5e5ee7a9ea04f21d9e320cf373c -a5d2011d2840a481d20718abf74c74a0 -cf5fb095f971de363362170021fb4b8d -5d10a529594e6db1de8afea66eac38fc -8c9aad74cf4b4d61664777030aa17624 -b563b6ffb93bbd804d97582ecac50452 -f881c18f78e21b0f03d1c776afee7231 -a68b21b1a75ec2ba2ce0dce4cbbc92eb -8b4eab6663ec57d08d148f9068f5dfc5 -b76d14ec69d796536dbd4272ff1a6aa9 -b7d08a340a6d4659f7fb8b527138c195 -fa7843c859643547376904efb5e80574 -f64589e088bfd14e61d50e079494dd25 -e2a2297fd8b26125d416604b06e2c124 -f57323874dbc4ff61d58abf3a79f42e4 -4898e57071cab888af7bfac3e99d36a5 -e9392bac717245fc60ba377c839cc1f1 -8005fb05838ec9693af09160c2b9b618 -7e2448291cf943aaad2668bf0d248d82 -9a0829583cd76088e8420477043131cc -0ed91581f0006615ad9aa71963bb3d84 -c81617c1c23cb1fe27de18697b18f940 -80eb786b92c530bc001167998e1d7324 -197fa36751da07f2b6e88f2af6728ba2 -6d233e2b7ace0cfd383c1da5cd2d0257 -ee45261b2e63647fa7cb9c273e9b744c -1400c8074b0595f1b97694cef02aaef3 -edb92f484985cb6717d7da6472b9edd7 -feb684fb89902c98f050b3c359780fba -8c17ea216cf7603dcc6f5a30463fe714 -400bfe1c5f3a2b7c7004a31edddb5be0 -06fe7a0bcac022808a2f7c7cce4044d8 -dedd66422b9390edd69548924c3f9412 -ff98b307886a47d3eef0f973fdbe0339 -1dd50fbc3c7d24689900e759d15ed2b9 -e00fde75649774724bd4e53dfc786bf8 -9edf3e927fe08c0d4c30e983b65a4fcd -4ac6f9a943ab1efdc9b5b9ce77c3a7bb -ce80a07afcd678cc2ad86b8111e45a82 -277ef7d98c04db9535b35d4a6a1bf084 -4803b9e16a79fd2589a23a7490469a25 -dc5d68f4f30bc524e9c9c5b27ac75dc9 -8353344388ebde887172f9f8d8ee2230 -4317f333de12d200416f4795f2605691 -bd94f949a3ae152c777cdcc54f63e868 -31724c959795198fcb6a91d36a0b4f8a -b123497b786e543b96388ce52772a19e -b25d6f33a09734438e57041c2de28f89 -dd1330c6cf399a4b8b253a42a51c1c25 -8689ab5d620e81a1bb780ace956a952a -03b1359856b98d0c74e32e5bd3f9aeaa -e88a3b4b04390578a822aa6a9352f454 -b191ef2795dcdfb7cda4856563e3d063 -70e47380329d0810f8be5672f0085734 -fac0c15cd23db124813df653a02e4de5 -5a41c20cd519647acbaf5ef61dca91ec -a3560c171cb5708e0e5c04bec51bb345 -4e3c2c667b473f51005dfb73070d71d8 -2f8fcf0fb7712c842357db2f3e4e9b24 -9ffe5ae3b1c41811028963402c1bd5be -31f90488a7babebb96f19f1ca079d5b0 -10265603c93d17d40d8f14c52df0e322 -203596f897f25f0a3be93423bcc7d97f -93c1e3f0ccc0976878bd22df42f1a49c -64962b542aeb93bb1d452f37ec722f34 -65fd65404bd903c4b9f63f68a023e457 -0ff26fb04bfb1757a5050d5c21f49c44 -04e5716e1dc27b9e337301f745d96ae3 -39bd5d3b785e6400e8cb820dada84999 -fb95b0580fa4fac6d11f8f45efcbf327 -03e8d84eae6faaf939e47505cad204ef -bec5008da01549b62c46336e3acf4d12 -efd9c3ee56e1511b122023c846c2fa83 -d58df167a6191a121bb55c08e44bf0e9 -a148d1f4e329183059c0d76b3d048626 -18de9ae666c3d2468c0ca57a99857f56 -e0364e5acfdae10255d9bb90669d50df -ba74123b44996ffe315e2f3f0d09e091 -ab24cf04ac8ef85fa3bf3278e5a89ec0 -57dbd73c311e7c2e4b262e03d8067b14 -343a48ebd0cdb24cb4c168be0dcd5e2f -47177abf3bee54bf44aea9a12851c3c9 -392cb5adc2ca46ef5977cc5adfbb087e -e0aeb03328b6f5ba412ddd6d2696c75d -5ee4af72ce7d914c30dc59c9a9584f62 -c3b02774e41fa791484c19ac02b89dc7 -3779fd5d979e734a3f8fc46c2e328a7f -d962d5f014ff2566782f36ad62887d54 -9fc736f94057d38bcab4b47e71e25e56 -fd943a9fa52b1462057be0e03f5c60ac -95466b7440e2ac69103fc7c154d7c9d3 -ce8a6177e9957aba902b7a40623dc24e -78a7d86087c08cc09248e0ca4c13657e -e20124697c1d7377f092d9 +db 08 73 79 73 74 69 74 6c 65 82 05 de 30 11 22 +33 44 b5 e5 ee 7a 9e a0 4f 21 d9 e3 20 cf 37 3c +a5 d2 01 1d 28 40 a4 81 d2 07 18 ab f7 4c 74 a0 +cf 5f b0 95 f9 71 de 36 33 62 17 00 21 fb 4b 8d +5d 10 a5 29 59 4e 6d b1 de 8a fe a6 6e ac 38 fc +8c 9a ad 74 cf 4b 4d 61 66 47 77 03 0a a1 76 24 +b5 63 b6 ff b9 3b bd 80 4d 97 58 2e ca c5 04 52 +f8 81 c1 8f 78 e2 1b 0f 03 d1 c7 76 af ee 72 31 +a6 8b 21 b1 a7 5e c2 ba 2c e0 dc e4 cb bc 92 eb +8b 4e ab 66 63 ec 57 d0 8d 14 8f 90 68 f5 df c5 +b7 6d 14 ec 69 d7 96 53 6d bd 42 72 ff 1a 6a a9 +b7 d0 8a 34 0a 6d 46 59 f7 fb 8b 52 71 38 c1 95 +fa 78 43 c8 59 64 35 47 37 69 04 ef b5 e8 05 74 +f6 45 89 e0 88 bf d1 4e 61 d5 0e 07 94 94 dd 25 +e2 a2 29 7f d8 b2 61 25 d4 16 60 4b 06 e2 c1 24 +f5 73 23 87 4d bc 4f f6 1d 58 ab f3 a7 9f 42 e4 +48 98 e5 70 71 ca b8 88 af 7b fa c3 e9 9d 36 a5 +e9 39 2b ac 71 72 45 fc 60 ba 37 7c 83 9c c1 f1 +80 05 fb 05 83 8e c9 69 3a f0 91 60 c2 b9 b6 18 +7e 24 48 29 1c f9 43 aa ad 26 68 bf 0d 24 8d 82 +9a 08 29 58 3c d7 60 88 e8 42 04 77 04 31 31 cc +0e d9 15 81 f0 00 66 15 ad 9a a7 19 63 bb 3d 84 +c8 16 17 c1 c2 3c b1 fe 27 de 18 69 7b 18 f9 40 +80 eb 78 6b 92 c5 30 bc 00 11 67 99 8e 1d 73 24 +19 7f a3 67 51 da 07 f2 b6 e8 8f 2a f6 72 8b a2 +6d 23 3e 2b 7a ce 0c fd 38 3c 1d a5 cd 2d 02 57 +ee 45 26 1b 2e 63 64 7f a7 cb 9c 27 3e 9b 74 4c +14 00 c8 07 4b 05 95 f1 b9 76 94 ce f0 2a ae f3 +ed b9 2f 48 49 85 cb 67 17 d7 da 64 72 b9 ed d7 +fe b6 84 fb 89 90 2c 98 f0 50 b3 c3 59 78 0f ba +8c 17 ea 21 6c f7 60 3d cc 6f 5a 30 46 3f e7 14 +40 0b fe 1c 5f 3a 2b 7c 70 04 a3 1e dd db 5b e0 +06 fe 7a 0b ca c0 22 80 8a 2f 7c 7c ce 40 44 d8 +de dd 66 42 2b 93 90 ed d6 95 48 92 4c 3f 94 12 +ff 98 b3 07 88 6a 47 d3 ee f0 f9 73 fd be 03 39 +1d d5 0f bc 3c 7d 24 68 99 00 e7 59 d1 5e d2 b9 +e0 0f de 75 64 97 74 72 4b d4 e5 3d fc 78 6b f8 +9e df 3e 92 7f e0 8c 0d 4c 30 e9 83 b6 5a 4f cd +4a c6 f9 a9 43 ab 1e fd c9 b5 b9 ce 77 c3 a7 bb +ce 80 a0 7a fc d6 78 cc 2a d8 6b 81 11 e4 5a 82 +27 7e f7 d9 8c 04 db 95 35 b3 5d 4a 6a 1b f0 84 +48 03 b9 e1 6a 79 fd 25 89 a2 3a 74 90 46 9a 25 +dc 5d 68 f4 f3 0b c5 24 e9 c9 c5 b2 7a c7 5d c9 +83 53 34 43 88 eb de 88 71 72 f9 f8 d8 ee 22 30 +43 17 f3 33 de 12 d2 00 41 6f 47 95 f2 60 56 91 +bd 94 f9 49 a3 ae 15 2c 77 7c dc c5 4f 63 e8 68 +31 72 4c 95 97 95 19 8f cb 6a 91 d3 6a 0b 4f 8a +b1 23 49 7b 78 6e 54 3b 96 38 8c e5 27 72 a1 9e +b2 5d 6f 33 a0 97 34 43 8e 57 04 1c 2d e2 8f 89 +dd 13 30 c6 cf 39 9a 4b 8b 25 3a 42 a5 1c 1c 25 +86 89 ab 5d 62 0e 81 a1 bb 78 0a ce 95 6a 95 2a +03 b1 35 98 56 b9 8d 0c 74 e3 2e 5b d3 f9 ae aa +e8 8a 3b 4b 04 39 05 78 a8 22 aa 6a 93 52 f4 54 +b1 91 ef 27 95 dc df b7 cd a4 85 65 63 e3 d0 63 +70 e4 73 80 32 9d 08 10 f8 be 56 72 f0 08 57 34 +fa c0 c1 5c d2 3d b1 24 81 3d f6 53 a0 2e 4d e5 +5a 41 c2 0c d5 19 64 7a cb af 5e f6 1d ca 91 ec +a3 56 0c 17 1c b5 70 8e 0e 5c 04 be c5 1b b3 45 +4e 3c 2c 66 7b 47 3f 51 00 5d fb 73 07 0d 71 d8 +2f 8f cf 0f b7 71 2c 84 23 57 db 2f 3e 4e 9b 24 +9f fe 5a e3 b1 c4 18 11 02 89 63 40 2c 1b d5 be +31 f9 04 88 a7 ba be bb 96 f1 9f 1c a0 79 d5 b0 +10 26 56 03 c9 3d 17 d4 0d 8f 14 c5 2d f0 e3 22 +20 35 96 f8 97 f2 5f 0a 3b e9 34 23 bc c7 d9 7f +93 c1 e3 f0 cc c0 97 68 78 bd 22 df 42 f1 a4 9c +64 96 2b 54 2a eb 93 bb 1d 45 2f 37 ec 72 2f 34 +65 fd 65 40 4b d9 03 c4 b9 f6 3f 68 a0 23 e4 57 +0f f2 6f b0 4b fb 17 57 a5 05 0d 5c 21 f4 9c 44 +04 e5 71 6e 1d c2 7b 9e 33 73 01 f7 45 d9 6a e3 +39 bd 5d 3b 78 5e 64 00 e8 cb 82 0d ad a8 49 99 +fb 95 b0 58 0f a4 fa c6 d1 1f 8f 45 ef cb f3 27 +03 e8 d8 4e ae 6f aa f9 39 e4 75 05 ca d2 04 ef +be c5 00 8d a0 15 49 b6 2c 46 33 6e 3a cf 4d 12 +ef d9 c3 ee 56 e1 51 1b 12 20 23 c8 46 c2 fa 83 +d5 8d f1 67 a6 19 1a 12 1b b5 5c 08 e4 4b f0 e9 +a1 48 d1 f4 e3 29 18 30 59 c0 d7 6b 3d 04 86 26 +18 de 9a e6 66 c3 d2 46 8c 0c a5 7a 99 85 7f 56 +e0 36 4e 5a cf da e1 02 55 d9 bb 90 66 9d 50 df +ba 74 12 3b 44 99 6f fe 31 5e 2f 3f 0d 09 e0 91 +ab 24 cf 04 ac 8e f8 5f a3 bf 32 78 e5 a8 9e c0 +57 db d7 3c 31 1e 7c 2e 4b 26 2e 03 d8 06 7b 14 +34 3a 48 eb d0 cd b2 4c b4 c1 68 be 0d cd 5e 2f +47 17 7a bf 3b ee 54 bf 44 ae a9 a1 28 51 c3 c9 +39 2c b5 ad c2 ca 46 ef 59 77 cc 5a df bb 08 7e +e0 ae b0 33 28 b6 f5 ba 41 2d dd 6d 26 96 c7 5d +5e e4 af 72 ce 7d 91 4c 30 dc 59 c9 a9 58 4f 62 +c3 b0 27 74 e4 1f a7 91 48 4c 19 ac 02 b8 9d c7 +37 79 fd 5d 97 9e 73 4a 3f 8f c4 6c 2e 32 8a 7f +d9 62 d5 f0 14 ff 25 66 78 2f 36 ad 62 88 7d 54 +9f c7 36 f9 40 57 d3 8b ca b4 b4 7e 71 e2 5e 56 +fd 94 3a 9f a5 2b 14 62 05 7b e0 e0 3f 5c 60 ac +95 46 6b 74 40 e2 ac 69 10 3f c7 c1 54 d7 c9 d3 +ce 8a 61 77 e9 95 7a ba 90 2b 7a 40 62 3d c2 4e +78 a7 d8 60 87 c0 8c c0 92 48 e0 ca 4c 13 65 7e +e2 01 24 69 7c 1d 73 77 f0 92 d9 diff --git a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt index 14cd579..0c625b4 100644 --- a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt +++ b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt @@ -1,95 +1,95 @@ -db087379737469746c658205de301122 -3344b5e5ee7a9ea04f21d9e320cf373c -a5d2011d2840a481d20718abf74c74a0 -cf5fb095f971de363362170021fb4b8d -5d10a529594e6db1de8afea66eac38fc -8c9aad74cf4b4d61664777030aa17624 -b563b6ffb93bbd804d97582ecac50452 -f881c18f78e21b0f03d1c776afee7231 -a68b21b1a75ec2ba2ce0dce4cbbc92eb -8b4eab6663ec57d08d148f9068f5dfc5 -b76d14ec69d796536dbd4272ff1a6aa9 -b7d08a340a6d4659f7fb8b527138c195 -fa7843c859643547376904efb5e80574 -f64589e088bfd14e61d50e079494dd25 -e2a2297fd8b26125d416604b06e2c124 -f57323874dbc4ff61d58abf3a79f42e4 -4898e57071cab888af7bfac3e99d36a5 -e9392bac717245fc60ba377c839cc1f1 -8005fb05838ec9693af09160c2b9b618 -7e2448291cf943aaad2668bf0d248d82 -9a0829583cd76088e8420477043131cc -0ed91581f0006615ad9aa71963bb3d84 -c81617c1c23cb1fe27de18697b18f940 -80eb786b92c530bc001167998e1d7324 -197fa36751da07f2b6e88f2af6728ba2 -6d233e2b7ace0cfd383c1da5cd2d0257 -ee45261b2e63647fa7cb9c273e9b744c -1400c8074b0595f1b97694cef02aaef3 -edb92f484985cb6717d7da6472b9edd7 -feb684fb89902c98f050b3c359780fba -8c17ea216cf7603dcc6f5a30463fe714 -400bfe1c5f3a2b7c7004a31edddb5be0 -06fe7a0bcac022808a2f7c7cce4044d8 -dedd66422b9390edd69548924c3f9412 -ff98b307886a47d3eef0f973fdbe0339 -1dd50fbc3c7d24689900e759d15ed2b9 -e00fde75649774724bd4e53dfc786bf8 -9edf3e927fe08c0d4c30e983b65a4fcd -4ac6f9a943ab1efdc9b5b9ce77c3a7bb -ce80a07afcd678cc2ad86b8111e45a82 -277ef7d98c04db9535b35d4a6a1bf084 -4803b9e16a79fd2589a23a7490469a25 -dc5d68f4f30bc524e9c9c5b27ac75dc9 -8353344388ebde887172f9f8d8ee2230 -4317f333de12d200416f4795f2605691 -bd94f949a3ae152c777cdcc54f63e868 -31724c959795198fcb6a91d36a0b4f8a -b123497b786e543b96388ce52772a19e -b25d6f33a09734438e57041c2de28f89 -dd1330c6cf399a4b8b253a42a51c1c25 -8689ab5d620e81a1bb780ace956a952a -03b1359856b98d0c74e32e5bd3f9aeaa -e88a3b4b04390578a822aa6a9352f454 -b191ef2795dcdfb7cda4856563e3d063 -70e47380329d0810f8be5672f0085734 -fac0c15cd23db124813df653a02e4de5 -5a41c20cd519647acbaf5ef61dca91ec -a3560c171cb5708e0e5c04bec51bb345 -4e3c2c667b473f51005dfb73070d71d8 -2f8fcf0fb7712c842357db2f3e4e9b24 -9ffe5ae3b1c41811028963402c1bd5be -31f90488a7babebb96f19f1ca079d5b0 -10265603c93d17d40d8f14c52df0e322 -203596f897f25f0a3be93423bcc7d97f -93c1e3f0ccc0976878bd22df42f1a49c -64962b542aeb93bb1d452f37ec722f34 -65fd65404bd903c4b9f63f68a023e457 -0ff26fb04bfb1757a5050d5c21f49c44 -04e5716e1dc27b9e337301f745d96ae3 -39bd5d3b785e6400e8cb820dada84999 -fb95b0580fa4fac6d11f8f45efcbf327 -03e8d84eae6faaf939e47505cad204ef -bec5008da01549b62c46336e3acf4d12 -efd9c3ee56e1511b122023c846c2fa83 -d58df167a6191a121bb55c08e44bf0e9 -a148d1f4e329183059c0d76b3d048626 -18de9ae666c3d2468c0ca57a99857f56 -e0364e5acfdae10255d9bb90669d50df -ba74123b44996ffe315e2f3f0d09e091 -ab24cf04ac8ef85fa3bf3278e5a89ec0 -57dbd73c311e7c2e4b262e03d8067b14 -343a48ebd0cdb24cb4c168be0dcd5e2f -47177abf3bee54bf44aea9a12851c3c9 -392cb5adc2ca46ef5977cc5adfbb087e -e0aeb03328b6f5ba412ddd6d2696c75d -5ee4af72ce7d914c30dc59c9a9584f62 -c3b02774e41fa791484c19ac02b89dc7 -3779fd5d979e734a3f8fc46c2e328a7f -d962d5f014ff2566782f36ad62887d54 -9fc736f94057d38bcab4b47e71e25e56 -fd943a9fa52b1462057be0e03f5c60ac -95466b7440e2ac69103fc7c154d7c9d3 -ce8a6177e9957aba902b7a40623dc24e -78a7d86087c08cc09248e0ca4c1365b5 -b44e22f56b44168a93770b +db 08 73 79 73 74 69 74 6c 65 82 05 de 30 11 22 +33 44 b5 e5 ee 7a 9e a0 4f 21 d9 e3 20 cf 37 3c +a5 d2 01 1d 28 40 a4 81 d2 07 18 ab f7 4c 74 a0 +cf 5f b0 95 f9 71 de 36 33 62 17 00 21 fb 4b 8d +5d 10 a5 29 59 4e 6d b1 de 8a fe a6 6e ac 38 fc +8c 9a ad 74 cf 4b 4d 61 66 47 77 03 0a a1 76 24 +b5 63 b6 ff b9 3b bd 80 4d 97 58 2e ca c5 04 52 +f8 81 c1 8f 78 e2 1b 0f 03 d1 c7 76 af ee 72 31 +a6 8b 21 b1 a7 5e c2 ba 2c e0 dc e4 cb bc 92 eb +8b 4e ab 66 63 ec 57 d0 8d 14 8f 90 68 f5 df c5 +b7 6d 14 ec 69 d7 96 53 6d bd 42 72 ff 1a 6a a9 +b7 d0 8a 34 0a 6d 46 59 f7 fb 8b 52 71 38 c1 95 +fa 78 43 c8 59 64 35 47 37 69 04 ef b5 e8 05 74 +f6 45 89 e0 88 bf d1 4e 61 d5 0e 07 94 94 dd 25 +e2 a2 29 7f d8 b2 61 25 d4 16 60 4b 06 e2 c1 24 +f5 73 23 87 4d bc 4f f6 1d 58 ab f3 a7 9f 42 e4 +48 98 e5 70 71 ca b8 88 af 7b fa c3 e9 9d 36 a5 +e9 39 2b ac 71 72 45 fc 60 ba 37 7c 83 9c c1 f1 +80 05 fb 05 83 8e c9 69 3a f0 91 60 c2 b9 b6 18 +7e 24 48 29 1c f9 43 aa ad 26 68 bf 0d 24 8d 82 +9a 08 29 58 3c d7 60 88 e8 42 04 77 04 31 31 cc +0e d9 15 81 f0 00 66 15 ad 9a a7 19 63 bb 3d 84 +c8 16 17 c1 c2 3c b1 fe 27 de 18 69 7b 18 f9 40 +80 eb 78 6b 92 c5 30 bc 00 11 67 99 8e 1d 73 24 +19 7f a3 67 51 da 07 f2 b6 e8 8f 2a f6 72 8b a2 +6d 23 3e 2b 7a ce 0c fd 38 3c 1d a5 cd 2d 02 57 +ee 45 26 1b 2e 63 64 7f a7 cb 9c 27 3e 9b 74 4c +14 00 c8 07 4b 05 95 f1 b9 76 94 ce f0 2a ae f3 +ed b9 2f 48 49 85 cb 67 17 d7 da 64 72 b9 ed d7 +fe b6 84 fb 89 90 2c 98 f0 50 b3 c3 59 78 0f ba +8c 17 ea 21 6c f7 60 3d cc 6f 5a 30 46 3f e7 14 +40 0b fe 1c 5f 3a 2b 7c 70 04 a3 1e dd db 5b e0 +06 fe 7a 0b ca c0 22 80 8a 2f 7c 7c ce 40 44 d8 +de dd 66 42 2b 93 90 ed d6 95 48 92 4c 3f 94 12 +ff 98 b3 07 88 6a 47 d3 ee f0 f9 73 fd be 03 39 +1d d5 0f bc 3c 7d 24 68 99 00 e7 59 d1 5e d2 b9 +e0 0f de 75 64 97 74 72 4b d4 e5 3d fc 78 6b f8 +9e df 3e 92 7f e0 8c 0d 4c 30 e9 83 b6 5a 4f cd +4a c6 f9 a9 43 ab 1e fd c9 b5 b9 ce 77 c3 a7 bb +ce 80 a0 7a fc d6 78 cc 2a d8 6b 81 11 e4 5a 82 +27 7e f7 d9 8c 04 db 95 35 b3 5d 4a 6a 1b f0 84 +48 03 b9 e1 6a 79 fd 25 89 a2 3a 74 90 46 9a 25 +dc 5d 68 f4 f3 0b c5 24 e9 c9 c5 b2 7a c7 5d c9 +83 53 34 43 88 eb de 88 71 72 f9 f8 d8 ee 22 30 +43 17 f3 33 de 12 d2 00 41 6f 47 95 f2 60 56 91 +bd 94 f9 49 a3 ae 15 2c 77 7c dc c5 4f 63 e8 68 +31 72 4c 95 97 95 19 8f cb 6a 91 d3 6a 0b 4f 8a +b1 23 49 7b 78 6e 54 3b 96 38 8c e5 27 72 a1 9e +b2 5d 6f 33 a0 97 34 43 8e 57 04 1c 2d e2 8f 89 +dd 13 30 c6 cf 39 9a 4b 8b 25 3a 42 a5 1c 1c 25 +86 89 ab 5d 62 0e 81 a1 bb 78 0a ce 95 6a 95 2a +03 b1 35 98 56 b9 8d 0c 74 e3 2e 5b d3 f9 ae aa +e8 8a 3b 4b 04 39 05 78 a8 22 aa 6a 93 52 f4 54 +b1 91 ef 27 95 dc df b7 cd a4 85 65 63 e3 d0 63 +70 e4 73 80 32 9d 08 10 f8 be 56 72 f0 08 57 34 +fa c0 c1 5c d2 3d b1 24 81 3d f6 53 a0 2e 4d e5 +5a 41 c2 0c d5 19 64 7a cb af 5e f6 1d ca 91 ec +a3 56 0c 17 1c b5 70 8e 0e 5c 04 be c5 1b b3 45 +4e 3c 2c 66 7b 47 3f 51 00 5d fb 73 07 0d 71 d8 +2f 8f cf 0f b7 71 2c 84 23 57 db 2f 3e 4e 9b 24 +9f fe 5a e3 b1 c4 18 11 02 89 63 40 2c 1b d5 be +31 f9 04 88 a7 ba be bb 96 f1 9f 1c a0 79 d5 b0 +10 26 56 03 c9 3d 17 d4 0d 8f 14 c5 2d f0 e3 22 +20 35 96 f8 97 f2 5f 0a 3b e9 34 23 bc c7 d9 7f +93 c1 e3 f0 cc c0 97 68 78 bd 22 df 42 f1 a4 9c +64 96 2b 54 2a eb 93 bb 1d 45 2f 37 ec 72 2f 34 +65 fd 65 40 4b d9 03 c4 b9 f6 3f 68 a0 23 e4 57 +0f f2 6f b0 4b fb 17 57 a5 05 0d 5c 21 f4 9c 44 +04 e5 71 6e 1d c2 7b 9e 33 73 01 f7 45 d9 6a e3 +39 bd 5d 3b 78 5e 64 00 e8 cb 82 0d ad a8 49 99 +fb 95 b0 58 0f a4 fa c6 d1 1f 8f 45 ef cb f3 27 +03 e8 d8 4e ae 6f aa f9 39 e4 75 05 ca d2 04 ef +be c5 00 8d a0 15 49 b6 2c 46 33 6e 3a cf 4d 12 +ef d9 c3 ee 56 e1 51 1b 12 20 23 c8 46 c2 fa 83 +d5 8d f1 67 a6 19 1a 12 1b b5 5c 08 e4 4b f0 e9 +a1 48 d1 f4 e3 29 18 30 59 c0 d7 6b 3d 04 86 26 +18 de 9a e6 66 c3 d2 46 8c 0c a5 7a 99 85 7f 56 +e0 36 4e 5a cf da e1 02 55 d9 bb 90 66 9d 50 df +ba 74 12 3b 44 99 6f fe 31 5e 2f 3f 0d 09 e0 91 +ab 24 cf 04 ac 8e f8 5f a3 bf 32 78 e5 a8 9e c0 +57 db d7 3c 31 1e 7c 2e 4b 26 2e 03 d8 06 7b 14 +34 3a 48 eb d0 cd b2 4c b4 c1 68 be 0d cd 5e 2f +47 17 7a bf 3b ee 54 bf 44 ae a9 a1 28 51 c3 c9 +39 2c b5 ad c2 ca 46 ef 59 77 cc 5a df bb 08 7e +e0 ae b0 33 28 b6 f5 ba 41 2d dd 6d 26 96 c7 5d +5e e4 af 72 ce 7d 91 4c 30 dc 59 c9 a9 58 4f 62 +c3 b0 27 74 e4 1f a7 91 48 4c 19 ac 02 b8 9d c7 +37 79 fd 5d 97 9e 73 4a 3f 8f c4 6c 2e 32 8a 7f +d9 62 d5 f0 14 ff 25 66 78 2f 36 ad 62 88 7d 54 +9f c7 36 f9 40 57 d3 8b ca b4 b4 7e 71 e2 5e 56 +fd 94 3a 9f a5 2b 14 62 05 7b e0 e0 3f 5c 60 ac +95 46 6b 74 40 e2 ac 69 10 3f c7 c1 54 d7 c9 d3 +ce 8a 61 77 e9 95 7a ba 90 2b 7a 40 62 3d c2 4e +78 a7 d8 60 87 c0 8c c0 92 48 e0 ca 4c 13 65 b5 +b4 4e 22 f5 6b 44 16 8a 93 77 0b diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json index aa11ffa..f365e47 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json @@ -1,30 +1,36 @@ { - "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0(test-/-test)\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", - "header": { - "identifier": "\\2MT382-1003", - "xxx": "ISk", - "z": "5" - }, - "metadata": { - "equipmentId": "00112233445566778899aabbccddeeff", + "dsmr": { + "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0(test-/-test)\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", + "header": { + "identifier": "\\2MT382-1003", + "xxx": "ISk", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(0999.00*kW)", "0-0:96.3.10(1)", "(13032.850)", "0-1:24.4.0(1)" - ], + ] + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "equipmentId": "00112233445566778899aabbccddeeff", "numericMessage": 0, "textMessage": "test-/-test" }, "electricity": { "tariffs": { "1": { - "received": 39837.604, - "returned": 5174.479 + "received": 39837604, + "returned": 5174479 }, "2": { - "received": 30477.225, - "returned": 11772.946 + "received": 30477225, + "returned": 11772946 } }, "currentTariff": 2, diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json index 664e07e..cf60e34 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json @@ -1,30 +1,36 @@ { - "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", - "header": { - "identifier": "\\2MT382-1003", - "xxx": "ISk", - "z": "5" - }, - "metadata": { - "equipmentId": "00112233445566778899aabbccddeeff", + "dsmr": { + "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", + "header": { + "identifier": "\\2MT382-1003", + "xxx": "ISk", + "z": "5" + }, "unknownLines": [ "0-0:17.0.0(0999.00*kW)", "0-0:96.3.10(1)", "(13032.850)", "0-1:24.4.0(1)" - ], + ] + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "equipmentId": "00112233445566778899aabbccddeeff", "numericMessage": 0, "textMessage": "" }, "electricity": { "tariffs": { "1": { - "received": 39837.604, - "returned": 5174.479 + "received": 39837604, + "returned": 5174479 }, "2": { - "received": 30477.225, - "returned": 11772.946 + "received": 30477225, + "returned": 11772946 } }, "currentTariff": 2, diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index 73db908..17209cf 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -1,12 +1,11 @@ { - "raw": "/KAM5\r\n\r\n0-0:1.0.0(250000000000W)\r\n1-0:1.8.0(00000123.321*kWh)\r\n1-0:2.8.0(00000456.654*kWh)\r\n1-0:3.8.0(00001234.432*kVArh)\r\n1-0:4.8.0(00005678.765*kVArh)\r\n1-0:1.7.0(0002.424*kW)\r\n1-0:2.7.0(0000.000*kW)\r\n1-0:3.7.0(0001.229*kVAr)\r\n1-0:4.7.0(0000.000*kVAr)\r\n1-0:21.7.0(0000.682*kW)\r\n1-0:41.7.0(0000.750*kW)\r\n1-0:61.7.0(0000.992*kW)\r\n1-0:22.7.0(0000.000*kW)\r\n1-0:42.7.0(0000.000*kW)\r\n1-0:62.7.0(0000.000*kW)\r\n1-0:23.7.0(0000.391*kVAr)\r\n1-0:43.7.0(0000.490*kVAr)\r\n1-0:63.7.0(0000.348*kVAr)\r\n1-0:24.7.0(0000.000*kVAr)\r\n1-0:44.7.0(0000.000*kVAr)\r\n1-0:64.7.0(0000.000*kVAr)\r\n1-0:32.7.0(225.4*V)\r\n1-0:52.7.0(229.4*V)\r\n1-0:72.7.0(225.9*V)\r\n1-0:31.7.0(003.4*A)\r\n1-0:51.7.0(003.9*A)\r\n1-0:71.7.0(004.6*A)\r\n!3AF8\r\n", - "header": { - "identifier": "", - "xxx": "KAM", - "z": "5" - }, - "metadata": { - "timestamp": "250000000000W", + "dsmr": { + "raw": "/KAM5\r\n\r\n0-0:1.0.0(250000000000W)\r\n1-0:1.8.0(00000123.321*kWh)\r\n1-0:2.8.0(00000456.654*kWh)\r\n1-0:3.8.0(00001234.432*kVArh)\r\n1-0:4.8.0(00005678.765*kVArh)\r\n1-0:1.7.0(0002.424*kW)\r\n1-0:2.7.0(0000.000*kW)\r\n1-0:3.7.0(0001.229*kVAr)\r\n1-0:4.7.0(0000.000*kVAr)\r\n1-0:21.7.0(0000.682*kW)\r\n1-0:41.7.0(0000.750*kW)\r\n1-0:61.7.0(0000.992*kW)\r\n1-0:22.7.0(0000.000*kW)\r\n1-0:42.7.0(0000.000*kW)\r\n1-0:62.7.0(0000.000*kW)\r\n1-0:23.7.0(0000.391*kVAr)\r\n1-0:43.7.0(0000.490*kVAr)\r\n1-0:63.7.0(0000.348*kVAr)\r\n1-0:24.7.0(0000.000*kVAr)\r\n1-0:44.7.0(0000.000*kVAr)\r\n1-0:64.7.0(0000.000*kVAr)\r\n1-0:32.7.0(225.4*V)\r\n1-0:52.7.0(229.4*V)\r\n1-0:72.7.0(225.9*V)\r\n1-0:31.7.0(003.4*A)\r\n1-0:51.7.0(003.9*A)\r\n1-0:71.7.0(004.6*A)\r\n!3AF8\r\n", + "header": { + "identifier": "", + "xxx": "KAM", + "z": "5" + }, "unknownLines": [ "1-0:3.8.0(00001234.432*kVArh)", "1-0:4.8.0(00005678.765*kVArh)", @@ -18,12 +17,23 @@ "1-0:24.7.0(0000.000*kVAr)", "1-0:44.7.0(0000.000*kVAr)", "1-0:64.7.0(0000.000*kVAr)" - ] + ], + "crc": { + "value": 15096, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "timestamp": "250000000000W" }, "electricity": { "total": { - "received": 123.321, - "returned": 456.654 + "received": 123321, + "returned": 456654 }, "powerReceivedTotal": 2.424, "powerReturnedTotal": 0, @@ -48,9 +58,5 @@ "l3": 4.6 } }, - "mBus": {}, - "crc": { - "value": 15096, - "valid": false - } + "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index 9b56a91..c19fa92 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -1,12 +1,11 @@ { - "raw": "/GRE5\\\\12341234_A\r\n\r\n0-0:1.0.0(123412341234W)\r\n0-0:0.0.0(123412341234)\r\n1-1:1.8.1(001595.070*kWh)\r\n1-1:1.8.2(005216.778*kWh)\r\n1-1:2.8.1(004478.038*kWh)\r\n1-1:2.8.2(000004.648*kWh)\r\n0-0:96.14.0(0001)\r\n1-1:1.7.0(01.622*kW)\r\n1-1:2.7.0(00.000*kW)\r\n0-0:96.7.21(00018)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(2)(0-0:96.7.19)(240523144934S)(0005564711*s)(241109083138W)(0000000124*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n1-0:32.7.0(238.9*V)\r\n1-0:52.7.0(231.9*V)\r\n1-0:72.7.0(239.7*V)\r\n1-0:31.7.0(005.15*A)\r\n1-0:51.7.0(017.19*A)\r\n1-0:71.7.0(005.02*A)\r\n0-0:96.13.0()\r\n0-0:96.1.4(12345)\r\n0-0:96.1.2(202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(01.175*kW)\r\n1-0:41.7.0(03.946*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(01.148*kW)\r\n0-0:96.13.1()\r\n1-1:1.6.0(250315014500W)(15.206*kW)\r\n0-0:98.1.0(12)\r\n1-0:1.4.0(00.288*kW)\r\n!3BA5\r\n", - "header": { - "identifier": "\\\\12341234_A", - "xxx": "GRE", - "z": "5" - }, - "metadata": { - "timestamp": "123412341234W", + "dsmr": { + "raw": "/GRE5\\\\12341234_A\r\n\r\n0-0:1.0.0(123412341234W)\r\n0-0:0.0.0(123412341234)\r\n1-1:1.8.1(001595.070*kWh)\r\n1-1:1.8.2(005216.778*kWh)\r\n1-1:2.8.1(004478.038*kWh)\r\n1-1:2.8.2(000004.648*kWh)\r\n0-0:96.14.0(0001)\r\n1-1:1.7.0(01.622*kW)\r\n1-1:2.7.0(00.000*kW)\r\n0-0:96.7.21(00018)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(2)(0-0:96.7.19)(240523144934S)(0005564711*s)(241109083138W)(0000000124*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n1-0:32.7.0(238.9*V)\r\n1-0:52.7.0(231.9*V)\r\n1-0:72.7.0(239.7*V)\r\n1-0:31.7.0(005.15*A)\r\n1-0:51.7.0(017.19*A)\r\n1-0:71.7.0(005.02*A)\r\n0-0:96.13.0()\r\n0-0:96.1.4(12345)\r\n0-0:96.1.2(202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(01.175*kW)\r\n1-0:41.7.0(03.946*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(01.148*kW)\r\n0-0:96.13.1()\r\n1-1:1.6.0(250315014500W)(15.206*kW)\r\n0-0:98.1.0(12)\r\n1-0:1.4.0(00.288*kW)\r\n!3BA5\r\n", + "header": { + "identifier": "\\\\12341234_A", + "xxx": "GRE", + "z": "5" + }, "unknownLines": [ "0-0:0.0.0(123412341234)", "1-0:99.97.0(2)(0-0:96.7.19)(240523144934S)(0005564711*s)(241109083138W)(0000000124*s)", @@ -16,6 +15,17 @@ "0-0:98.1.0(12)", "1-0:1.4.0(00.288*kW)" ], + "crc": { + "value": 15269, + "valid": false + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] + }, + "metadata": { + "timestamp": "123412341234W", "events": { "powerFailures": 18, "longPowerFailures": 4, @@ -36,12 +46,12 @@ "electricity": { "tariffs": { "1": { - "received": 1595.07, - "returned": 4478.038 + "received": 1595070, + "returned": 4478038 }, "2": { - "received": 5216.778, - "returned": 4.648 + "received": 5216778, + "returned": 4648 } }, "currentTariff": 1, @@ -68,9 +78,5 @@ "l3": 1.148 } }, - "mBus": {}, - "crc": { - "value": 15269, - "valid": false - } + "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/unknown-xmx-1.json b/tests/telegrams/dsmr/unknown-xmx-1.json index 7fc919b..1b2060f 100644 --- a/tests/telegrams/dsmr/unknown-xmx-1.json +++ b/tests/telegrams/dsmr/unknown-xmx-1.json @@ -1,9 +1,15 @@ { - "raw": "/XMX5XMXABCE000012345\r\n\r\n0-0:96.1.1(0123456789ABCDEF)\r\n1-0:1.8.1(11667.440*kWh)\r\n1-0:1.8.2(11781.558*kWh)\r\n1-0:2.8.1(00000.000*kWh)\r\n1-0:2.8.2(00000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.55*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!", - "header": { - "identifier": "XMXABCE000012345", - "xxx": "XMX", - "z": "5" + "dsmr": { + "raw": "/XMX5XMXABCE000012345\r\n\r\n0-0:96.1.1(0123456789ABCDEF)\r\n1-0:1.8.1(11667.440*kWh)\r\n1-0:1.8.2(11781.558*kWh)\r\n1-0:2.8.1(00000.000*kWh)\r\n1-0:2.8.2(00000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.55*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!", + "header": { + "identifier": "XMXABCE000012345", + "xxx": "XMX", + "z": "5" + } + }, + "cosem": { + "unknownObjects": [], + "knownObjects": [] }, "metadata": { "equipmentId": "0123456789ABCDEF", @@ -13,11 +19,11 @@ "electricity": { "tariffs": { "1": { - "received": 11667.44, + "received": 11667440, "returned": 0 }, "2": { - "received": 11781.558, + "received": 11781558, "returned": 0 } }, diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index 9e2c049..583cac7 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -6,7 +6,6 @@ */ import fs from 'fs/promises'; import { - bufferToHexString, encryptFrame, getAllDLMSTestTelegramTestCases, getAllDSMRTestTelegramTestCases, From 8d32b9dcbb93557c8d2fa8472c4910ee5278e615 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Wed, 21 May 2025 17:02:51 +0200 Subject: [PATCH 03/18] feat(dlms): add more tests and add timeout to dlms/cosem parser --- src/protocols/dlms.ts | 1 + src/stream/stream-dlms.ts | 31 ++- tests/protocols/dsmr.spec.ts | 12 +- tests/protocols/encryption.spec.ts | 4 +- tests/stream/stream-detect-type.spec.ts | 6 +- tests/stream/stream-dlms.spec.ts | 215 ++++++++++++++++++ tests/stream/stream-dsmr.spec.ts | 31 ++- tests/telegrams/dlms/aidon-example-1.json | 100 ++++---- .../dlms/aidon-example-2-encrypted.json | 14 +- tests/telegrams/dlms/aidon-example-2.json | 160 +++++++------ .../encrypted/aidon-example-2-with-aad.txt | 80 ++++--- .../encrypted/aidon-example-2-without-aad.txt | 80 ++++--- tests/telegrams/dlms/kamstrup-example-1.json | 106 +++++---- tests/telegrams/dlms/kamstrup-example-2.json | 124 +++++----- tests/telegrams/dlms/kamstrup-example-3.json | 92 ++++---- tests/test-utils.ts | 19 +- tools/decrypt-telegram.ts | 5 +- tools/update-test-telegrams.ts | 19 +- 18 files changed, 673 insertions(+), 426 deletions(-) create mode 100644 tests/stream/stream-dlms.spec.ts diff --git a/src/protocols/dlms.ts b/src/protocols/dlms.ts index b118b9c..c27f239 100644 --- a/src/protocols/dlms.ts +++ b/src/protocols/dlms.ts @@ -23,6 +23,7 @@ * https://www.gurux.fi/GuruxDLMSTranslator */ +import { bufferToHexString } from '../../tests/test-utils.js'; import { decryptDlmsFrame, ENCRYPTED_DLMS_TELEGRAM_SOF } from '../protocols/encryption.js'; import { SmartMeterDecryptionRequired, SmartMeterUnknownMessageTypeError } from '../util/errors.js'; import { DlmsDataTypes, getDlmsObjectCount } from './dlms-datatype.js'; diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index 19c6adb..b307aa8 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -10,7 +10,7 @@ import { HDLC_TELEGRAM_SOF_EOF, HdlcParserResult, } from './../protocols/hdlc.js'; -import { SmartMeterError, StartOfFrameNotFoundError } from '../util/errors.js'; +import { SmartMeterError, SmartMeterTimeoutError, StartOfFrameNotFoundError } from '../util/errors.js'; import { decodeDLMSContent, decodeDlmsObis } from './../protocols/dlms.js'; import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; @@ -21,20 +21,30 @@ export type DlmsStreamParserOptions = { decryptionKey?: Buffer; /** AAD */ additionalAuthenticatedData?: Buffer; + /** + * Maximum time in milliseconds to wait for a full frame to be received. The timer starts when a + * valid start of frame/header is received. + */ + fullFrameRequiredWithinMs?: number; }; export class DlmsStreamParser implements SmartMeterStreamParser { public readonly startOfFrameByte = HDLC_TELEGRAM_SOF_EOF; private hasStartOfFrame = false; + private fullFrameRequiredWithinMs: number; + private fullFrameRequiredTimeout?: NodeJS.Timeout; private telegram = Buffer.alloc(0); private cachedContent = Buffer.alloc(0); private header: ReturnType | undefined = undefined; private readonly boundOnData = this.onData.bind(this); + private readonly boundOnFullFrameRequiredTimeout = this.onFullFrameRequiredTimeout.bind(this); constructor(private options: DlmsStreamParserOptions) { this.options.stream.addListener('data', this.boundOnData); + + this.fullFrameRequiredWithinMs = options.fullFrameRequiredWithinMs ?? 5000; } private onData(data: Buffer) { @@ -46,8 +56,13 @@ export class DlmsStreamParser implements SmartMeterStreamParser { error.withRawTelegram(data); this.options.callback(error, undefined); + return; } + this.fullFrameRequiredTimeout = setTimeout( + this.boundOnFullFrameRequiredTimeout, + this.fullFrameRequiredWithinMs, + ); this.telegram = data.subarray(sofIndex, data.length); this.hasStartOfFrame = true; } else { @@ -109,6 +124,8 @@ export class DlmsStreamParser implements SmartMeterStreamParser { return; } + clearTimeout(this.fullFrameRequiredTimeout); + try { const content = Buffer.concat([ this.cachedContent, @@ -186,12 +203,22 @@ export class DlmsStreamParser implements SmartMeterStreamParser { } } + private onFullFrameRequiredTimeout() { + const error = new SmartMeterTimeoutError(); + error.withRawTelegram(this.telegram); + this.options.callback(error, undefined); + + // Reset the entire state here, as the full frame was not received. + this.clear(); + } + destroy(): void { this.options.stream.removeListener('data', this.boundOnData); this.clear(); } clear(): void { + clearTimeout(this.fullFrameRequiredTimeout); this.hasStartOfFrame = false; this.header = undefined; this.telegram = Buffer.alloc(0); @@ -199,6 +226,6 @@ export class DlmsStreamParser implements SmartMeterStreamParser { } currentSize(): number { - throw new Error('Method not implemented.'); + return this.telegram.length + this.cachedContent.length; } } diff --git a/tests/protocols/dsmr.spec.ts b/tests/protocols/dsmr.spec.ts index 72b3548..01d81bd 100644 --- a/tests/protocols/dsmr.spec.ts +++ b/tests/protocols/dsmr.spec.ts @@ -4,7 +4,7 @@ import { DSMR } from '../../src/index.js'; import { encryptFrame, getAllDSMRTestTelegramTestCases, - readTelegramFromFiles, + readDsmrTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY, } from './../test-utils.js'; @@ -15,7 +15,7 @@ describe('DSMR', async () => { for (const testCase of testCases) { it(`Parses ${testCase}`, async () => { - const { input, output: expectedOutput } = await readTelegramFromFiles( + const { input, output: expectedOutput } = await readDsmrTelegramFromFiles( `./tests/telegrams/dsmr/${testCase}`, ); @@ -28,7 +28,7 @@ describe('DSMR', async () => { }); it(`Parses ${testCase} with decryption (valid AAD)`, async () => { - const { input, output: expectedOutput } = await readTelegramFromFiles( + const { input, output: expectedOutput } = await readDsmrTelegramFromFiles( `./tests/telegrams/dsmr/${testCase}`, ); @@ -44,7 +44,7 @@ describe('DSMR', async () => { }); it(`Parses ${testCase} with decryption (missing AAD)`, async () => { - const { input, output: expectedOutput } = await readTelegramFromFiles( + const { input, output: expectedOutput } = await readDsmrTelegramFromFiles( `./tests/telegrams/dsmr/${testCase}`, ); @@ -60,7 +60,7 @@ describe('DSMR', async () => { }); it(`Parses ${testCase} with decryption (invalid AAD)`, async () => { - const { input, output: expectedOutput } = await readTelegramFromFiles( + const { input, output: expectedOutput } = await readDsmrTelegramFromFiles( `./tests/telegrams/dsmr/${testCase}`, ); @@ -77,7 +77,7 @@ describe('DSMR', async () => { } it('Gets m-bus data', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); const parsed = parseDsmr({ telegram: input }); diff --git a/tests/protocols/encryption.spec.ts b/tests/protocols/encryption.spec.ts index 9e1a00e..678e9a5 100644 --- a/tests/protocols/encryption.spec.ts +++ b/tests/protocols/encryption.spec.ts @@ -2,7 +2,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert'; import { readHexFile, - readTelegramFromFiles, + readDsmrTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY, } from './../test-utils.js'; @@ -11,7 +11,7 @@ import { SmartMeterDecryptionError } from '../../src/index.js'; import { parseDsmr } from '../../src/protocols/dsmr.js'; describe('Encryption', async () => { - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-luxembourgh-spec-example', ); const encryptedWithAad = await readHexFile( diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts index bef513e..e5a4861 100644 --- a/tests/stream/stream-detect-type.spec.ts +++ b/tests/stream/stream-detect-type.spec.ts @@ -1,12 +1,12 @@ import assert from 'node:assert'; import { PassThrough } from 'node:stream'; import { describe, it, mock } from 'node:test'; -import { chunkBuffer, readHexFile, readTelegramFromFiles } from './../test-utils.js'; +import { chunkBuffer, readHexFile, readDsmrTelegramFromFiles } from './../test-utils.js'; import { StreamDetectType } from '../../src/stream/stream-detect-type.js'; describe('Stream: Detect Type', () => { it('Detects unencrypted DSMR telegrams', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); const stream = new PassThrough(); const callback = mock.fn(); @@ -25,7 +25,7 @@ describe('Stream: Detect Type', () => { }); it('Detects unencrypted DSMR telegrams (chunks)', async () => { - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); const stream = new PassThrough(); const callback = mock.fn(); diff --git a/tests/stream/stream-dlms.spec.ts b/tests/stream/stream-dlms.spec.ts new file mode 100644 index 0000000..812a0c7 --- /dev/null +++ b/tests/stream/stream-dlms.spec.ts @@ -0,0 +1,215 @@ +import assert from 'node:assert'; +import { describe, it, mock } from 'node:test'; + +import { chunkBuffer, readDlmsTelegramFromFiles, readHexFile, TEST_AAD, TEST_DECRYPTION_KEY } from '../test-utils.js'; +import { PassThrough } from 'stream'; +import { DlmsStreamParser, SmartMeterDecryptionError, SmartMeterTimeoutError, StartOfFrameNotFoundError } from '../../src/index.js'; +import { HDLC_HEADER_LENGTH, HDLC_TELEGRAM_SOF_EOF, HdlcParserResult } from '../../src/protocols/hdlc.js'; + +describe('Stream DLMS', () => { + const testDlmsStreamParser = (input: Buffer) => { + const stream = new PassThrough(); + const callback = mock.fn(); + + const instance = new DlmsStreamParser({ + stream, callback, + }); + + stream.write(input); + stream.end(); + instance.destroy(); + + return callback.mock.calls; + } + + describe('Unencrypted', () => { + it('Parses a chunked unencrypted telegram', async () => { + const { input, output } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-2'); + + const chunks = chunkBuffer(input, 10); + const stream = new PassThrough(); + const callback = mock.fn(); + + const instance = new DlmsStreamParser({ + stream, callback, + }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + instance.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); + assert.deepStrictEqual(callback.mock.calls[0].arguments[1], JSON.parse(JSON.stringify(output))); + }); + + it('Parses two unencrypted telegrams', async () => { + const { input: input1, output: output1 } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-1'); + const { input: input2, output: output2 } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-2'); + + const calls = testDlmsStreamParser(Buffer.concat([input1, input2])); + + assert.deepStrictEqual(calls.length, 2); + assert.deepStrictEqual(calls[0].arguments[0], null); + assert.deepStrictEqual(calls[0].arguments[1], output1); + assert.deepStrictEqual(calls[1].arguments[0], null); + assert.deepStrictEqual(calls[1].arguments[1], output2); + }); + + it('Throws error when telegram is invalid', async () => { + const data = 'invalid telegram xxx yyy'; + assert.ok(data.length > HDLC_HEADER_LENGTH); + + const calls = testDlmsStreamParser(Buffer.from(data)); + + assert.equal(calls.length, 1); + assert.ok(calls[0].arguments[0] instanceof StartOfFrameNotFoundError); + assert.equal(calls[0].arguments[1], undefined); + }); + + it('Throws error if a full frame is not received in time', async (context) => { + const { input } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-2'); + + context.mock.timers.enable(); + + const fullFrameRequiredWithinMs = 5000; + + const stream = new PassThrough(); + const callback = mock.fn(); + + const instance = new DlmsStreamParser({ + stream, callback, fullFrameRequiredWithinMs, + }); + + const numberOfChunks = 5; + const chunkSize = 5; + + // Start sending small bits of data, but it should't be enough to complete the frame. + for (let i = 0; i < numberOfChunks; i++) { + const chunk = input.subarray(i * chunkSize, (i + 1) * chunkSize); + stream.write(chunk); + assert.equal(callback.mock.calls.length, 0); + + context.mock.timers.tick(fullFrameRequiredWithinMs / numberOfChunks); + } + + // Here it should have timed out and called the callback with an error. + assert.equal(callback.mock.calls.length, 1); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + + // Writing invalid data should now throw an error. + stream.write('invalid data'); + + // And not trigger a timeout. + context.mock.timers.tick(fullFrameRequiredWithinMs); + + assert.equal(callback.mock.calls.length, 2); + assert.ok(callback.mock.calls[1].arguments[0] instanceof StartOfFrameNotFoundError); + assert.equal(instance.currentSize(), 0); + + instance.destroy(); + }); + + it('Throws error if a full frame is not received in time 2', async (context) => { + context.mock.timers.enable(); + + const fullFrameRequiredWithinMs = 5000; + + const stream = new PassThrough(); + const callback = mock.fn(); + + const instance = new DlmsStreamParser({ + stream, callback, fullFrameRequiredWithinMs, + }); + + stream.write(Buffer.from([HDLC_TELEGRAM_SOF_EOF])); // Start by writing the start of the telegram + + context.mock.timers.tick(fullFrameRequiredWithinMs); + + // Here it should have timed out and called the callback with an error. + assert.equal(callback.mock.calls.length, 1); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(instance.currentSize(), 0); + + instance.destroy(); + }); + }); + + describe('Encrypted', () => { + it('Throws an error if key is invalid', async () => { + const stream = new PassThrough(); + const callback = mock.fn(); + const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + + const instance = new DlmsStreamParser({ + stream, + callback, + decryptionKey: Buffer.from('invalid-key12345', 'ascii'), + additionalAuthenticatedData: TEST_AAD, + }); + + stream.write(input); + + stream.end(); + instance.destroy(); + + assert.equal(callback.mock.calls.length, 1); + assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterDecryptionError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + }); + + it('Parses when AAD is invalid', async () => { + const stream = new PassThrough(); + const callback = mock.fn(); + const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + + const instance = new DlmsStreamParser({ + stream, + callback, + decryptionKey: TEST_DECRYPTION_KEY, + additionalAuthenticatedData: Buffer.from('invalid-key12345', 'ascii'), + }); + + stream.write(input); + + stream.end(); + instance.destroy(); + + assert.equal(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); + const result = callback.mock.calls[0].arguments[1] as HdlcParserResult; + + assert.equal(result.additionalAuthenticatedDataValid, false); + }); + + it('Parses when AAD is missing', async () => { + const stream = new PassThrough(); + const callback = mock.fn(); + const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + + const instance = new DlmsStreamParser({ + stream, + callback, + decryptionKey: TEST_DECRYPTION_KEY, + additionalAuthenticatedData: undefined, + }); + + stream.write(input); + + stream.end(); + instance.destroy(); + + assert.equal(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); + + const result = callback.mock.calls[0].arguments[1] as HdlcParserResult; + assert.equal(result.additionalAuthenticatedDataValid, false); + }); + }); +}); \ No newline at end of file diff --git a/tests/stream/stream-dsmr.spec.ts b/tests/stream/stream-dsmr.spec.ts index ac6932a..c9a648d 100644 --- a/tests/stream/stream-dsmr.spec.ts +++ b/tests/stream/stream-dsmr.spec.ts @@ -5,7 +5,7 @@ import { chunkBuffer, chunkString, encryptFrame, - readTelegramFromFiles, + readDsmrTelegramFromFiles, TEST_AAD, TEST_DECRYPTION_KEY, } from './../test-utils.js'; @@ -44,7 +44,7 @@ const assertDecryptedFrameValid = ({ describe('DSMRStreamParser', () => { describe('Unencrypted', () => { it('Parses a chunked unencrypted telegram', async () => { - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); @@ -67,10 +67,10 @@ describe('DSMRStreamParser', () => { }); it('Parses two unencrypted telegrams', async () => { - const { input: input1, output: output1 } = await readTelegramFromFiles( + const { input: input1, output: output1 } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); - const { input: input2, output: output2 } = await readTelegramFromFiles( + const { input: input2, output: output2 } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-4.0-spec-example', ); @@ -113,7 +113,7 @@ describe('DSMRStreamParser', () => { it("Doesn't throw error after receiving null character", async () => { // Note: some meters send a null character (\0) at the end of the telegram. This should be ignored. - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); @@ -207,7 +207,7 @@ describe('DSMRStreamParser', () => { it('Parses when the CRC line is missing', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( 'tests/telegrams/dsmr/iskra-mt-382-no-crc', ); @@ -235,9 +235,8 @@ describe('DSMRStreamParser', () => { it('Immediately parses when CRC is missing and a 2nd telegram is received', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( 'tests/telegrams/dsmr/iskra-mt-382-no-crc', - true, ); const stream = new PassThrough(); @@ -269,7 +268,7 @@ describe('DSMRStreamParser', () => { it('Immediately parses when CRC is missing and a three telegrams are received', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( 'tests/telegrams/dsmr/iskra-mt-382-no-crc', ); @@ -305,7 +304,7 @@ describe('DSMRStreamParser', () => { it('Handles text messages', async (context) => { context.mock.timers.enable(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( 'tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message', ); @@ -333,7 +332,7 @@ describe('DSMRStreamParser', () => { describe('Encrypted', () => { it('Parses a chunked encrypted telegram', async () => { - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); @@ -365,10 +364,10 @@ describe('DSMRStreamParser', () => { }); it('Parses two encrypted telegrams', async () => { - const { input: input1, output: output1 } = await readTelegramFromFiles( + const { input: input1, output: output1 } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); - const { input: input2, output: output2 } = await readTelegramFromFiles( + const { input: input2, output: output2 } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-4.0-spec-example', ); @@ -488,7 +487,7 @@ describe('DSMRStreamParser', () => { it('Throws an error if key is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const { input } = await readTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); const instance = new EncryptedDSMRStreamParser({ @@ -511,7 +510,7 @@ describe('DSMRStreamParser', () => { it('Parses when AAD is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); @@ -541,7 +540,7 @@ describe('DSMRStreamParser', () => { it('Parses when AAD is missing', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const { input, output } = await readTelegramFromFiles( + const { input, output } = await readDsmrTelegramFromFiles( './tests/telegrams/dsmr/dsmr-5.0-spec-example', ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); diff --git a/tests/telegrams/dlms/aidon-example-1.json b/tests/telegrams/dlms/aidon-example-1.json index 4953395..27ee5c0 100644 --- a/tests/telegrams/dlms/aidon-example-1.json +++ b/tests/telegrams/dlms/aidon-example-1.json @@ -1,56 +1,54 @@ -[ - { - "hdlc": { - "raw": "7ea0d24108831382d6e6e7000f40000000000109020209060101000281ff0a0b4149444f4e5f5630303031020209060000600100ff0a1037333539393932383930393431373432020209060000600107ff0a0436353135020309060100010700ff060000055202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000003e402020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10005d02020fff1621020309060100200700ff1209c402020fff1623e0c47e", - "header": { - "destinationAddress": 32, - "sourceAddress": 577, - "crc": { - "value": 54914, - "valid": true - } - }, +{ + "hdlc": { + "raw": "7ea0d24108831382d6e6e7000f40000000000109020209060101000281ff0a0b4149444f4e5f5630303031020209060000600100ff0a1037333539393932383930393431373432020209060000600107ff0a0436353135020309060100010700ff060000055202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000003e402020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10005d02020fff1621020309060100200700ff1209c402020fff1623e0c47e", + "header": { + "destinationAddress": 32, + "sourceAddress": 577, "crc": { - "value": 50400, + "value": 54914, "valid": true } }, - "dlms": { - "invokeId": 0, - "timestamp": "", - "unknownObjects": [], - "payloadType": "BasicStructure" + "crc": { + "value": 50400, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicStructure" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.2.129(AIDON_V0001)", + "0-0:96.1.7(6515)", + "1-0:3.7.0(996*var)", + "1-0:4.7.0(0*var)" + ], + "knownObjects": [ + "0-0:96.1.0(7359992890941742)", + "1-0:1.7.0(1362*W)", + "1-0:2.7.0(0*W)", + "1-0:31.7.0(9.3*A)", + "1-0:32.7.0(250*V)" + ] + }, + "electricity": { + "powerReceivedTotal": 1362000, + "powerReturnedTotal": 0, + "current": { + "l1": 9.3 }, - "cosem": { - "unknownObjects": [ - "1-1:0.2.129(AIDON_V0001)", - "0-0:96.1.7(6515)", - "1-0:3.7.0(996*var)", - "1-0:4.7.0(0*var)" - ], - "knownObjects": [ - "0-0:96.1.0(7359992890941742)", - "1-0:1.7.0(1362*W)", - "1-0:2.7.0(0*W)", - "1-0:31.7.0(9.3*A)", - "1-0:32.7.0(250*V)" - ] - }, - "electricity": { - "powerReceivedTotal": 1362000, - "powerReturnedTotal": 0, - "current": { - "l1": 9.3 - }, - "voltage": { - "l1": 250 - } - }, - "mBus": { - "0": { - "equipmentId": "7359992890941742" - } - }, - "metadata": {} - } -] \ No newline at end of file + "voltage": { + "l1": 250 + } + }, + "mBus": { + "0": { + "equipmentId": "7359992890941742" + } + }, + "metadata": {} +} \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.json b/tests/telegrams/dlms/aidon-example-2-encrypted.json index decc1e8..e094f79 100644 --- a/tests/telegrams/dlms/aidon-example-2-encrypted.json +++ b/tests/telegrams/dlms/aidon-example-2-encrypted.json @@ -1,9 +1,7 @@ -[ - { - "error": { - "message": "Encrypted frame detected", - "name": "DecryptionRequired", - "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:58:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:124:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" - } +{ + "error": { + "message": "Encrypted frame detected", + "name": "DecryptionRequired", + "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:59:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:141:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" } -] \ No newline at end of file +} \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2.json b/tests/telegrams/dlms/aidon-example-2.json index fe085c5..21a3755 100644 --- a/tests/telegrams/dlms/aidon-example-2.json +++ b/tests/telegrams/dlms/aidon-example-2.json @@ -1,90 +1,88 @@ -[ - { - "hdlc": { - "raw": "7ea2434108831385ebe6e7000f4000000000011b020209060000010000ff090c07e30c1001073b28ff8000ff020309060100010700ff060000046202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000005e302020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10000002020fff1621020309060100330700ff10004b02020fff1621020309060100470700ff10000002020fff1621020309060100200700ff12090302020fff1623020309060100340700ff1209c302020fff1623020309060100480700ff12090402020fff1623020309060100150700ff060000000002020f00161b020309060100160700ff060000000002020f00161b020309060100170700ff060000000002020f00161d020309060100180700ff060000000002020f00161d020309060100290700ff060000046202020f00161b0203090601002a0700ff060000000002020f00161b0203090601002b0700ff06000005e202020f00161d0203090601002c0700ff060000000002020f00161d0203090601003d0700ff060000000002020f00161b0203090601003e0700ff060000000002020f00161b0203090601003f0700ff060000000002020f00161d020309060100400700ff060000000002020f00161d020309060100010800ff060099598602020f00161e020309060100020800ff060000000802020f00161e020309060100030800ff060064ed4b02020f001620020309060100040800ff060000000502020f001620be407e", - "header": { - "destinationAddress": 32, - "sourceAddress": 577, - "crc": { - "value": 60293, - "valid": true - } - }, +{ + "hdlc": { + "raw": "7ea2434108831385ebe6e7000f4000000000011b020209060000010000ff090c07e30c1001073b28ff8000ff020309060100010700ff060000046202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000005e302020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10000002020fff1621020309060100330700ff10004b02020fff1621020309060100470700ff10000002020fff1621020309060100200700ff12090302020fff1623020309060100340700ff1209c302020fff1623020309060100480700ff12090402020fff1623020309060100150700ff060000000002020f00161b020309060100160700ff060000000002020f00161b020309060100170700ff060000000002020f00161d020309060100180700ff060000000002020f00161d020309060100290700ff060000046202020f00161b0203090601002a0700ff060000000002020f00161b0203090601002b0700ff06000005e202020f00161d0203090601002c0700ff060000000002020f00161d0203090601003d0700ff060000000002020f00161b0203090601003e0700ff060000000002020f00161b0203090601003f0700ff060000000002020f00161d020309060100400700ff060000000002020f00161d020309060100010800ff060099598602020f00161e020309060100020800ff060000000802020f00161e020309060100030800ff060064ed4b02020f001620020309060100040800ff060000000502020f001620be407e", + "header": { + "destinationAddress": 32, + "sourceAddress": 577, "crc": { - "value": 16574, + "value": 60293, "valid": true } }, - "dlms": { - "invokeId": 0, - "timestamp": "", - "unknownObjects": [], - "payloadType": "BasicStructure" + "crc": { + "value": 16574, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicStructure" + }, + "cosem": { + "unknownObjects": [ + "1-0:3.7.0(1507*var)", + "1-0:4.7.0(0*var)", + "1-0:23.7.0(0*var)", + "1-0:24.7.0(0*var)", + "1-0:43.7.0(1506*var)", + "1-0:44.7.0(0*var)", + "1-0:63.7.0(0*var)", + "1-0:64.7.0(0*var)", + "1-0:3.8.0(6614347*varh)", + "1-0:4.8.0(5*varh)" + ], + "knownObjects": [ + "0-0:1.0.0(07e30c1001073b28ff8000ff)", + "1-0:1.7.0(1122*W)", + "1-0:2.7.0(0*W)", + "1-0:31.7.0(0*A)", + "1-0:51.7.0(7.5*A)", + "1-0:71.7.0(0*A)", + "1-0:32.7.0(230.70000000000002*V)", + "1-0:52.7.0(249.9*V)", + "1-0:72.7.0(230.8*V)", + "1-0:21.7.0(0*W)", + "1-0:22.7.0(0*W)", + "1-0:41.7.0(1122*W)", + "1-0:42.7.0(0*W)", + "1-0:61.7.0(0*W)", + "1-0:62.7.0(0*W)", + "1-0:1.8.0(10049926*Wh)", + "1-0:2.8.0(8*Wh)" + ] + }, + "electricity": { + "powerReceivedTotal": 1122000, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 7.5, + "l3": 0 }, - "cosem": { - "unknownObjects": [ - "1-0:3.7.0(1507*var)", - "1-0:4.7.0(0*var)", - "1-0:23.7.0(0*var)", - "1-0:24.7.0(0*var)", - "1-0:43.7.0(1506*var)", - "1-0:44.7.0(0*var)", - "1-0:63.7.0(0*var)", - "1-0:64.7.0(0*var)", - "1-0:3.8.0(6614347*varh)", - "1-0:4.8.0(5*varh)" - ], - "knownObjects": [ - "0-0:1.0.0(07e30c1001073b28ff8000ff)", - "1-0:1.7.0(1122*W)", - "1-0:2.7.0(0*W)", - "1-0:31.7.0(0*A)", - "1-0:51.7.0(7.5*A)", - "1-0:71.7.0(0*A)", - "1-0:32.7.0(230.70000000000002*V)", - "1-0:52.7.0(249.9*V)", - "1-0:72.7.0(230.8*V)", - "1-0:21.7.0(0*W)", - "1-0:22.7.0(0*W)", - "1-0:41.7.0(1122*W)", - "1-0:42.7.0(0*W)", - "1-0:61.7.0(0*W)", - "1-0:62.7.0(0*W)", - "1-0:1.8.0(10049926*Wh)", - "1-0:2.8.0(8*Wh)" - ] + "voltage": { + "l1": 230.70000000000002, + "l2": 249.9, + "l3": 230.8 }, - "electricity": { - "powerReceivedTotal": 1122000, - "powerReturnedTotal": 0, - "current": { - "l1": 0, - "l2": 7.5, - "l3": 0 - }, - "voltage": { - "l1": 230.70000000000002, - "l2": 249.9, - "l3": 230.8 - }, - "powerReceived": { - "l1": 0, - "l2": 1122, - "l3": 0 - }, - "powerReturned": { - "l1": 0, - "l2": 0, - "l3": 0 - }, - "total": { - "received": 10049926, - "returned": 8 - } + "powerReceived": { + "l1": 0, + "l2": 1122, + "l3": 0 + }, + "powerReturned": { + "l1": 0, + "l2": 0, + "l3": 0 }, - "mBus": {}, - "metadata": { - "timestamp": "07e30c1001073b28ff8000ff" + "total": { + "received": 10049926, + "returned": 8 } + }, + "mBus": {}, + "metadata": { + "timestamp": "07e30c1001073b28ff8000ff" } -] \ No newline at end of file +} \ No newline at end of file diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt index ebb2d9c..e5af3b2 100644 --- a/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt @@ -1,41 +1,39 @@ -7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 -74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b -02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d -78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 -9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 -7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 -7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 -01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f -d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 -70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b -e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 -e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 -58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 -41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 -a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 -c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f -9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe -e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 -4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 -bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 -d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d -41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 -38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec -1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f -cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 -ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 -f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 -20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc -32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 -b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 -bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 -c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c -cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 -f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e -a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 -9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 -4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 -bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 -7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 -9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d 09 ee -97 c8 31 de 81 eb dd c2 ca cb dc 76 7e +7e a2 60 03 05 00 03 3b e6 e7 00 db 08 73 79 73 +74 69 74 6c 65 82 02 47 30 11 22 33 44 95 e9 9b +02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c ef 2b +7d 8e 51 e4 27 37 9e e2 5c a3 14 fd 89 bf 9c c0 +5a ef 0c 03 4b 27 d1 17 d3 79 b9 0f 25 97 10 68 +68 42 82 e8 b4 ab 8e 63 a4 0f d1 43 a6 99 46 e1 +7b 61 53 41 72 52 2c 39 96 48 11 85 50 85 cb 8a +f7 8b b3 7a a1 82 1f f8 f9 34 77 d5 b0 f0 b5 4b +d0 2c 38 30 f8 35 7a 9e c3 42 0b 95 a7 16 9f 81 +6b f0 89 15 d6 ef d1 fa 8d a7 27 b1 25 fc 0c 48 +ee a2 f7 81 26 b6 ab 40 cc f1 c6 98 5d db cc 59 +ac a4 7f 52 76 64 79 96 4e 0b 86 bb da fc 1e 3a +a8 65 77 cf d7 b9 75 be 1e d0 a7 c9 41 6b f1 6a +71 18 2c be 1a 7f 84 9e e7 00 ba cd 56 b1 d7 af +81 fe 7e 7d e2 3e c8 b6 ab 2d 09 d7 95 e7 43 90 +c6 03 5e ba 3e 6d 09 30 cf 0e 0c cd 59 16 ab 72 +6b 69 e5 31 6b 92 c0 8c f4 00 ca 45 6d d2 5d 41 +f0 8a a4 9a 5a ca fd c2 af 1b 9c df 08 01 d1 21 +5b b7 f0 51 97 07 46 b2 b0 f9 df a6 36 c9 36 a4 +b8 f8 59 07 9c e7 fe b6 90 bb 12 4f 0b 7a 1c 28 +c1 69 86 9e 07 5e 8e 23 0c ba b2 4f 65 5f 39 4e +fe 6f 80 d7 6f 22 50 37 05 16 fa 21 e9 14 b6 c6 +c6 4e 25 87 f5 93 5a 48 b9 37 a3 fe 24 2e f1 ea +0a 9f e4 08 ee d7 41 4b 2a c8 6a e9 bf 36 42 89 +d4 03 92 39 2d 57 a9 8b 2d 5d eb 37 4f 93 4c 83 +e8 35 d7 dd a8 d3 01 f8 71 bc 8e 5c 35 0a 1a ab +fc 22 cd 10 0d 2f 97 e8 6c 3d 62 c1 76 19 2e 05 +22 70 75 ad 04 b7 17 04 a2 42 60 25 21 f9 21 78 +36 bf f5 b5 47 b9 c1 cd 13 67 db de 97 1e 78 63 +b7 ed 6a 0b fc f5 55 41 86 da f9 f2 9f b5 34 a7 +a0 05 95 fa 62 9c fc 63 57 24 96 b8 30 de 11 44 +87 57 0d 0a 50 73 3d 4c 0e c8 26 75 38 da 2f 6e +0b 0c 4a 59 34 92 26 e9 14 74 ed 95 96 d1 39 f2 +fa 10 b8 a7 1f 51 45 e0 71 74 ea e6 ee b0 49 21 +a2 bd d5 ee a0 75 bc 69 13 b8 21 de ae 82 37 bb +50 6e 21 e2 c1 b0 ae 8c 8f 33 18 2e f5 01 8e 17 +44 12 59 a9 34 c6 54 24 69 ff 89 da 3f ee 59 58 +a1 4c 62 12 bf 22 f3 ff 5c 29 df bb fe da f9 6d +eb 7e diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt index 705ec66..b4fc684 100644 --- a/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt @@ -1,41 +1,39 @@ -7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 -74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b -02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d -78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 -9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 -7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 -7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 -01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f -d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 -70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b -e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 -e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 -58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 -41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 -a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 -c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f -9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe -e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 -4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 -bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 -d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d -41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 -38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec -1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f -cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 -ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 -f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 -20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc -32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 -b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 -bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 -c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c -cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 -f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e -a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 -9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 -4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 -bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 -7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 -9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d bf ca -ae 62 b0 d6 69 89 48 e6 88 94 f6 0e 7e +7e a2 60 03 05 00 03 3b e6 e7 00 db 08 73 79 73 +74 69 74 6c 65 82 02 47 30 11 22 33 44 95 e9 9b +02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c ef 2b +7d 8e 51 e4 27 37 9e e2 5c a3 14 fd 89 bf 9c c0 +5a ef 0c 03 4b 27 d1 17 d3 79 b9 0f 25 97 10 68 +68 42 82 e8 b4 ab 8e 63 a4 0f d1 43 a6 99 46 e1 +7b 61 53 41 72 52 2c 39 96 48 11 85 50 85 cb 8a +f7 8b b3 7a a1 82 1f f8 f9 34 77 d5 b0 f0 b5 4b +d0 2c 38 30 f8 35 7a 9e c3 42 0b 95 a7 16 9f 81 +6b f0 89 15 d6 ef d1 fa 8d a7 27 b1 25 fc 0c 48 +ee a2 f7 81 26 b6 ab 40 cc f1 c6 98 5d db cc 59 +ac a4 7f 52 76 64 79 96 4e 0b 86 bb da fc 1e 3a +a8 65 77 cf d7 b9 75 be 1e d0 a7 c9 41 6b f1 6a +71 18 2c be 1a 7f 84 9e e7 00 ba cd 56 b1 d7 af +81 fe 7e 7d e2 3e c8 b6 ab 2d 09 d7 95 e7 43 90 +c6 03 5e ba 3e 6d 09 30 cf 0e 0c cd 59 16 ab 72 +6b 69 e5 31 6b 92 c0 8c f4 00 ca 45 6d d2 5d 41 +f0 8a a4 9a 5a ca fd c2 af 1b 9c df 08 01 d1 21 +5b b7 f0 51 97 07 46 b2 b0 f9 df a6 36 c9 36 a4 +b8 f8 59 07 9c e7 fe b6 90 bb 12 4f 0b 7a 1c 28 +c1 69 86 9e 07 5e 8e 23 0c ba b2 4f 65 5f 39 4e +fe 6f 80 d7 6f 22 50 37 05 16 fa 21 e9 14 b6 c6 +c6 4e 25 87 f5 93 5a 48 b9 37 a3 fe 24 2e f1 ea +0a 9f e4 08 ee d7 41 4b 2a c8 6a e9 bf 36 42 89 +d4 03 92 39 2d 57 a9 8b 2d 5d eb 37 4f 93 4c 83 +e8 35 d7 dd a8 d3 01 f8 71 bc 8e 5c 35 0a 1a ab +fc 22 cd 10 0d 2f 97 e8 6c 3d 62 c1 76 19 2e 05 +22 70 75 ad 04 b7 17 04 a2 42 60 25 21 f9 21 78 +36 bf f5 b5 47 b9 c1 cd 13 67 db de 97 1e 78 63 +b7 ed 6a 0b fc f5 55 41 86 da f9 f2 9f b5 34 a7 +a0 05 95 fa 62 9c fc 63 57 24 96 b8 30 de 11 44 +87 57 0d 0a 50 73 3d 4c 0e c8 26 75 38 da 2f 6e +0b 0c 4a 59 34 92 26 e9 14 74 ed 95 96 d1 39 f2 +fa 10 b8 a7 1f 51 45 e0 71 74 ea e6 ee b0 49 21 +a2 bd d5 ee a0 75 bc 69 13 b8 21 de ae 82 37 bb +50 6e 21 e2 c1 b0 ae 8c 8f 33 18 2e f5 01 8e 17 +44 12 59 a9 34 c6 54 24 69 ff 89 da 3f ee 59 58 +a1 4c 62 53 05 59 1f 1d d7 6d 10 46 5b 88 9f 26 +74 7e diff --git a/tests/telegrams/dlms/kamstrup-example-1.json b/tests/telegrams/dlms/kamstrup-example-1.json index 7b66d5f..2252049 100644 --- a/tests/telegrams/dlms/kamstrup-example-1.json +++ b/tests/telegrams/dlms/kamstrup-example-1.json @@ -1,59 +1,57 @@ -[ - { - "hdlc": { - "raw": "7ea0e22b2113239ae6e7000f000000000c07d0010106162100ff80000102190a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff1200005be57e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 39459, - "valid": true - } - }, +{ + "hdlc": { + "raw": "7ea0e22b2113239ae6e7000f000000000c07d0010106162100ff80000102190a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff1200005be57e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, "crc": { - "value": 58715, + "value": 39459, "valid": true } }, - "dlms": { - "invokeId": 0, - "timestamp": "", - "unknownObjects": [], - "payloadType": "BasicList" + "crc": { + "value": 58715, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)", + "1-1:3.7.0(0)", + "1-1:4.7.0(0)" + ], + "knownObjects": [ + "1-1:1.7.0(0)", + "1-1:2.7.0(0)", + "1-1:31.7.0(0)", + "1-1:51.7.0(0)", + "1-1:71.7.0(0)", + "1-1:32.7.0(0)", + "1-1:52.7.0(0)", + "1-1:72.7.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 0, + "l3": 0 }, - "cosem": { - "unknownObjects": [ - "1-1:0.0.5(5706567000000000)", - "1-1:96.1.1(000000000000000000)", - "1-1:3.7.0(0)", - "1-1:4.7.0(0)" - ], - "knownObjects": [ - "1-1:1.7.0(0)", - "1-1:2.7.0(0)", - "1-1:31.7.0(0)", - "1-1:51.7.0(0)", - "1-1:71.7.0(0)", - "1-1:32.7.0(0)", - "1-1:52.7.0(0)", - "1-1:72.7.0(0)" - ] - }, - "electricity": { - "powerReceivedTotal": 0, - "powerReturnedTotal": 0, - "current": { - "l1": 0, - "l2": 0, - "l3": 0 - }, - "voltage": { - "l1": 0, - "l2": 0, - "l3": 0 - } - }, - "mBus": {}, - "metadata": {} - } -] \ No newline at end of file + "voltage": { + "l1": 0, + "l2": 0, + "l3": 0 + } + }, + "mBus": {}, + "metadata": {} +} \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-2.json b/tests/telegrams/dlms/kamstrup-example-2.json index 6daea88..3dde605 100644 --- a/tests/telegrams/dlms/kamstrup-example-2.json +++ b/tests/telegrams/dlms/kamstrup-example-2.json @@ -1,69 +1,67 @@ -[ - { - "hdlc": { - "raw": "7ea12c2b2113fc04e6e7000f000000000c07e1081003100005ff80000002230a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000009060101020800ff060000000009060101030800ff060000000009060101040800ff0600000000c8867e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 1276, - "valid": true - } - }, +{ + "hdlc": { + "raw": "7ea12c2b2113fc04e6e7000f000000000c07e1081003100005ff80000002230a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000009060101020800ff060000000009060101030800ff060000000009060101040800ff0600000000c8867e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, "crc": { - "value": 34504, + "value": 1276, "valid": true } }, - "dlms": { - "invokeId": 0, - "timestamp": "", - "unknownObjects": [ - "octet_string: 07e1081003100005ff800000" - ], - "payloadType": "BasicList" + "crc": { + "value": 34504, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [ + "octet_string: 07e1081003100005ff800000" + ], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)", + "1-1:3.7.0(0)", + "1-1:4.7.0(0)", + "1-1:3.8.0(0)", + "1-1:4.8.0(0)" + ], + "knownObjects": [ + "1-1:1.7.0(0)", + "1-1:2.7.0(0)", + "1-1:31.7.0(0)", + "1-1:51.7.0(0)", + "1-1:71.7.0(0)", + "1-1:32.7.0(0)", + "1-1:52.7.0(0)", + "1-1:72.7.0(0)", + "1-1:1.8.0(0)", + "1-1:2.8.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 0, + "l3": 0 }, - "cosem": { - "unknownObjects": [ - "1-1:0.0.5(5706567000000000)", - "1-1:96.1.1(000000000000000000)", - "1-1:3.7.0(0)", - "1-1:4.7.0(0)", - "1-1:3.8.0(0)", - "1-1:4.8.0(0)" - ], - "knownObjects": [ - "1-1:1.7.0(0)", - "1-1:2.7.0(0)", - "1-1:31.7.0(0)", - "1-1:51.7.0(0)", - "1-1:71.7.0(0)", - "1-1:32.7.0(0)", - "1-1:52.7.0(0)", - "1-1:72.7.0(0)", - "1-1:1.8.0(0)", - "1-1:2.8.0(0)" - ] + "voltage": { + "l1": 0, + "l2": 0, + "l3": 0 }, - "electricity": { - "powerReceivedTotal": 0, - "powerReturnedTotal": 0, - "current": { - "l1": 0, - "l2": 0, - "l3": 0 - }, - "voltage": { - "l1": 0, - "l2": 0, - "l3": 0 - }, - "total": { - "received": 0, - "returned": 0 - } - }, - "mBus": {}, - "metadata": {} - } -] \ No newline at end of file + "total": { + "received": 0, + "returned": 0 + } + }, + "mBus": {}, + "metadata": {} +} \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-3.json b/tests/telegrams/dlms/kamstrup-example-3.json index 1a66104..49ee689 100644 --- a/tests/telegrams/dlms/kamstrup-example-3.json +++ b/tests/telegrams/dlms/kamstrup-example-3.json @@ -1,53 +1,51 @@ -[ - { - "hdlc": { - "raw": "7ea0ae2b2113a01be6e7000f000000000c07e1081003100005ff800000020f0a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff0600000000090601011f0700ff060000000009060101200700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000005217e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 7072, - "valid": true - } - }, +{ + "hdlc": { + "raw": "7ea0ae2b2113a01be6e7000f000000000c07e1081003100005ff800000020f0a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff0600000000090601011f0700ff060000000009060101200700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000005217e", + "header": { + "destinationAddress": 21, + "sourceAddress": 16, "crc": { - "value": 8453, + "value": 7072, "valid": true } }, - "dlms": { - "invokeId": 0, - "timestamp": "", - "unknownObjects": [ - "octet_string: 07e1081003100005ff800000" - ], - "payloadType": "BasicList" + "crc": { + "value": 8453, + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [ + "octet_string: 07e1081003100005ff800000" + ], + "payloadType": "BasicList" + }, + "cosem": { + "unknownObjects": [ + "1-1:0.0.5(5706567000000000)", + "1-1:96.1.1(000000000000000000)" + ], + "knownObjects": [ + "1-1:1.7.0(0)", + "1-1:31.7.0(0)", + "1-1:32.7.0(0)", + "1-1:1.8.0(0)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "current": { + "l1": 0 }, - "cosem": { - "unknownObjects": [ - "1-1:0.0.5(5706567000000000)", - "1-1:96.1.1(000000000000000000)" - ], - "knownObjects": [ - "1-1:1.7.0(0)", - "1-1:31.7.0(0)", - "1-1:32.7.0(0)", - "1-1:1.8.0(0)" - ] + "voltage": { + "l1": 0 }, - "electricity": { - "powerReceivedTotal": 0, - "current": { - "l1": 0 - }, - "voltage": { - "l1": 0 - }, - "total": { - "received": 0 - } - }, - "mBus": {}, - "metadata": {} - } -] \ No newline at end of file + "total": { + "received": 0 + } + }, + "mBus": {}, + "metadata": {} +} \ No newline at end of file diff --git a/tests/test-utils.ts b/tests/test-utils.ts index c1b97a4..21b0087 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -24,16 +24,26 @@ export const getAllDLMSTestTelegramTestCases = async () => { return files.filter((file) => file.endsWith('.txt')).map((file) => file.replace('.txt', '')); }; -export const readTelegramFromFiles = async (path: string, replaceNewLines = true) => { +export const readDsmrTelegramFromFiles = async (path: string) => { const input = await fs.readFile(`${path}.txt`); const output = await fs.readFile(`${path}.json`); return { - input: replaceNewLines ? input.toString().replace(/\r?\n/g, '\r\n') : input.toString(), + input: input.toString().replace(/\r?\n/g, '\r\n'), output: JSON.parse(output.toString()) as object, }; }; +export const readDlmsTelegramFromFiles = async (path: string) => { + const input = await readHexFile(`${path}.txt`); + const output = await fs.readFile(`${path}.json`); + + return { + input, + output: JSON.parse(output.toString()) as object, + }; +} + export const readHexFile = async (path: string) => { const file = await fs.readFile(path, 'utf-8'); @@ -143,13 +153,16 @@ export const encryptFrame = ({ aad, systemTitle, frameCounter, + frameStringEncoding, }: { - frame: string; + frame: Buffer | string; key: Buffer; aad?: Buffer; systemTitle?: Buffer; frameCounter?: Buffer; + frameStringEncoding?: BufferEncoding; }) => { + frame = Buffer.isBuffer(frame) ? frame : Buffer.from(frame, frameStringEncoding ?? 'utf-8'); systemTitle ??= Buffer.from('systitle', 'ascii'); // Note: for reproducing the same frame, the frame counter is always the same. // Real meters will change this every frame. diff --git a/tools/decrypt-telegram.ts b/tools/decrypt-telegram.ts index 8c2e962..13bf699 100644 --- a/tools/decrypt-telegram.ts +++ b/tools/decrypt-telegram.ts @@ -2,7 +2,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; -import { TEST_AAD, TEST_DECRYPTION_KEY } from '../tests/test-utils.js'; +import { TEST_AAD, TEST_DECRYPTION_KEY, writeHexFile } from '../tests/test-utils.js'; import { decryptDlmsFrame } from '../src/protocols/encryption.js'; const inputPath = process.argv[2]; @@ -51,7 +51,8 @@ const { header, footer, content, error } = decryptDlmsFrame({ const resolvedOutputPath = path.resolve(process.cwd(), outputPath); -await fs.writeFile(resolvedOutputPath, content, 'binary'); +// await fs.writeFile(resolvedOutputPath, content, 'binary'); +await writeHexFile(resolvedOutputPath, content); console.log('Telegram decrypted'); console.log('Header fields:'); diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index 583cac7..a2ad0c4 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -43,7 +43,7 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr input = input.replace(/\r?\n/g, '\r\n'); const encryptedAad = encryptFrame({ - frame: input, + frame: Buffer.from(input, 'utf-8'), key: TEST_DECRYPTION_KEY, aad: TEST_AAD, }); @@ -54,7 +54,7 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr ); const encryptedWithoutAad = encryptFrame({ - frame: input, + frame: Buffer.from(input, 'utf-8'), key: TEST_DECRYPTION_KEY, }); @@ -101,7 +101,11 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr await new Promise((resolve) => passthrough.write(input, resolve)); - const json = JSON.stringify(results, null, 2); + if (results.length !== 1) { + console.warn('Warning: more than one result found!'); + } + + const json = JSON.stringify(results[0], null, 2); await fs.writeFile(`./tests/telegrams/dlms/${file}.json`, json); parser.destroy(); } @@ -117,7 +121,9 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr const hdlcHeader = decodeHdlcHeader(input); const frame = input.subarray(0, hdlcHeader.frameLength + 2); - const frameContent = frame.subarray(hdlcHeader.consumedBytes) + const frameContent = frame.subarray(hdlcHeader.consumedBytes); + + console.log(`frameContent`, frameContent.subarray(0, 10)) const llc = decodeLlcHeader(frameContent); const content = frame.subarray( @@ -126,13 +132,14 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr ); const encryptedAad = encryptFrame({ - frame: content.toString('binary'), + frame: content, + // frame: Buffer.from('Hello, world! 12345678901234567890123456789'), key: TEST_DECRYPTION_KEY, aad: TEST_AAD, }); const encryptedWithoutAad = encryptFrame({ - frame: content.toString('binary'), + frame: content, key: TEST_DECRYPTION_KEY, }); From f5aa6665b50d72954f6d20b64516202eb23be17b Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Wed, 21 May 2025 17:06:22 +0200 Subject: [PATCH 04/18] chore: linting --- src/protocols/dlms.ts | 1 - src/protocols/dsmr.ts | 10 +--- src/stream/stream-dlms.ts | 8 +-- tests/protocols/dsmr.spec.ts | 4 +- tests/stream/stream-detect-type.spec.ts | 8 ++- tests/stream/stream-dlms.spec.ts | 70 +++++++++++++++++++------ tests/stream/stream-dsmr.spec.ts | 4 +- tests/test-utils.ts | 32 ++++++----- tools/parse-dlms.ts | 48 ++++++++++++++--- tools/parse-hdlc.ts | 51 ++++++++++++++---- tools/update-test-telegrams.ts | 20 +++---- 11 files changed, 184 insertions(+), 72 deletions(-) diff --git a/src/protocols/dlms.ts b/src/protocols/dlms.ts index c27f239..b118b9c 100644 --- a/src/protocols/dlms.ts +++ b/src/protocols/dlms.ts @@ -23,7 +23,6 @@ * https://www.gurux.fi/GuruxDLMSTranslator */ -import { bufferToHexString } from '../../tests/test-utils.js'; import { decryptDlmsFrame, ENCRYPTED_DLMS_TELEGRAM_SOF } from '../protocols/encryption.js'; import { SmartMeterDecryptionRequired, SmartMeterUnknownMessageTypeError } from '../util/errors.js'; import { DlmsDataTypes, getDlmsObjectCount } from './dlms-datatype.js'; diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts index 7809fc9..6a68611 100644 --- a/src/protocols/dsmr.ts +++ b/src/protocols/dsmr.ts @@ -1,7 +1,7 @@ import { decryptDlmsFrame } from './encryption.js'; import { SmartMeterParserError } from '../util/errors.js'; import { CosemLibrary } from './cosem.js'; -import { obisCodeToString, parseObisCodeFromString } from './obis-code.js'; +import { parseObisCodeFromString } from './obis-code.js'; import { calculateCrc16Arc } from '../util/crc.js'; import { BaseParserResult } from '../util/base-result.js'; @@ -62,13 +62,7 @@ export const DEFAULT_FRAME_ENCODING = 'binary'; * @param enteredCrc * @returns */ -export const isDsmrCrcValid = ({ - telegram, - crc, -}: { - telegram: string; - crc: number; -}) => { +export const isDsmrCrcValid = ({ telegram, crc }: { telegram: string; crc: number }) => { // Strip the CRC from the telegram const telegramParts = telegram.split(`${CRLF}!`); const strippedTelegram = telegramParts[0] + CRLF + '!'; diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index b307aa8..8783c92 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-console */ - import { Readable } from 'stream'; import { decodeHdlcFooter, @@ -10,7 +8,11 @@ import { HDLC_TELEGRAM_SOF_EOF, HdlcParserResult, } from './../protocols/hdlc.js'; -import { SmartMeterError, SmartMeterTimeoutError, StartOfFrameNotFoundError } from '../util/errors.js'; +import { + SmartMeterError, + SmartMeterTimeoutError, + StartOfFrameNotFoundError, +} from '../util/errors.js'; import { decodeDLMSContent, decodeDlmsObis } from './../protocols/dlms.js'; import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; diff --git a/tests/protocols/dsmr.spec.ts b/tests/protocols/dsmr.spec.ts index 01d81bd..f026184 100644 --- a/tests/protocols/dsmr.spec.ts +++ b/tests/protocols/dsmr.spec.ts @@ -77,7 +77,9 @@ describe('DSMR', async () => { } it('Gets m-bus data', async () => { - const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles( + './tests/telegrams/dsmr/dsmr-5.0-spec-example', + ); const parsed = parseDsmr({ telegram: input }); diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts index e5a4861..5d95665 100644 --- a/tests/stream/stream-detect-type.spec.ts +++ b/tests/stream/stream-detect-type.spec.ts @@ -6,7 +6,9 @@ import { StreamDetectType } from '../../src/stream/stream-detect-type.js'; describe('Stream: Detect Type', () => { it('Detects unencrypted DSMR telegrams', async () => { - const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles( + './tests/telegrams/dsmr/dsmr-5.0-spec-example', + ); const stream = new PassThrough(); const callback = mock.fn(); @@ -25,7 +27,9 @@ describe('Stream: Detect Type', () => { }); it('Detects unencrypted DSMR telegrams (chunks)', async () => { - const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles( + './tests/telegrams/dsmr/dsmr-5.0-spec-example', + ); const stream = new PassThrough(); const callback = mock.fn(); diff --git a/tests/stream/stream-dlms.spec.ts b/tests/stream/stream-dlms.spec.ts index 812a0c7..8a5566a 100644 --- a/tests/stream/stream-dlms.spec.ts +++ b/tests/stream/stream-dlms.spec.ts @@ -1,10 +1,25 @@ import assert from 'node:assert'; import { describe, it, mock } from 'node:test'; -import { chunkBuffer, readDlmsTelegramFromFiles, readHexFile, TEST_AAD, TEST_DECRYPTION_KEY } from '../test-utils.js'; +import { + chunkBuffer, + readDlmsTelegramFromFiles, + readHexFile, + TEST_AAD, + TEST_DECRYPTION_KEY, +} from '../test-utils.js'; import { PassThrough } from 'stream'; -import { DlmsStreamParser, SmartMeterDecryptionError, SmartMeterTimeoutError, StartOfFrameNotFoundError } from '../../src/index.js'; -import { HDLC_HEADER_LENGTH, HDLC_TELEGRAM_SOF_EOF, HdlcParserResult } from '../../src/protocols/hdlc.js'; +import { + DlmsStreamParser, + SmartMeterDecryptionError, + SmartMeterTimeoutError, + StartOfFrameNotFoundError, +} from '../../src/index.js'; +import { + HDLC_HEADER_LENGTH, + HDLC_TELEGRAM_SOF_EOF, + HdlcParserResult, +} from '../../src/protocols/hdlc.js'; describe('Stream DLMS', () => { const testDlmsStreamParser = (input: Buffer) => { @@ -12,7 +27,8 @@ describe('Stream DLMS', () => { const callback = mock.fn(); const instance = new DlmsStreamParser({ - stream, callback, + stream, + callback, }); stream.write(input); @@ -20,18 +36,21 @@ describe('Stream DLMS', () => { instance.destroy(); return callback.mock.calls; - } + }; describe('Unencrypted', () => { it('Parses a chunked unencrypted telegram', async () => { - const { input, output } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-2'); + const { input, output } = await readDlmsTelegramFromFiles( + './tests/telegrams/dlms/aidon-example-2', + ); const chunks = chunkBuffer(input, 10); const stream = new PassThrough(); const callback = mock.fn(); const instance = new DlmsStreamParser({ - stream, callback, + stream, + callback, }); for (const chunk of chunks) { @@ -43,12 +62,19 @@ describe('Stream DLMS', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], JSON.parse(JSON.stringify(output))); + assert.deepStrictEqual( + callback.mock.calls[0].arguments[1], + JSON.parse(JSON.stringify(output)), + ); }); it('Parses two unencrypted telegrams', async () => { - const { input: input1, output: output1 } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-1'); - const { input: input2, output: output2 } = await readDlmsTelegramFromFiles('./tests/telegrams/dlms/aidon-example-2'); + const { input: input1, output: output1 } = await readDlmsTelegramFromFiles( + './tests/telegrams/dlms/aidon-example-1', + ); + const { input: input2, output: output2 } = await readDlmsTelegramFromFiles( + './tests/telegrams/dlms/aidon-example-2', + ); const calls = testDlmsStreamParser(Buffer.concat([input1, input2])); @@ -81,7 +107,9 @@ describe('Stream DLMS', () => { const callback = mock.fn(); const instance = new DlmsStreamParser({ - stream, callback, fullFrameRequiredWithinMs, + stream, + callback, + fullFrameRequiredWithinMs, }); const numberOfChunks = 5; @@ -123,11 +151,13 @@ describe('Stream DLMS', () => { const callback = mock.fn(); const instance = new DlmsStreamParser({ - stream, callback, fullFrameRequiredWithinMs, + stream, + callback, + fullFrameRequiredWithinMs, }); stream.write(Buffer.from([HDLC_TELEGRAM_SOF_EOF])); // Start by writing the start of the telegram - + context.mock.timers.tick(fullFrameRequiredWithinMs); // Here it should have timed out and called the callback with an error. @@ -143,7 +173,9 @@ describe('Stream DLMS', () => { it('Throws an error if key is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + const input = await readHexFile( + './tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt', + ); const instance = new DlmsStreamParser({ stream, @@ -165,7 +197,9 @@ describe('Stream DLMS', () => { it('Parses when AAD is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + const input = await readHexFile( + './tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt', + ); const instance = new DlmsStreamParser({ stream, @@ -190,7 +224,9 @@ describe('Stream DLMS', () => { it('Parses when AAD is missing', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const input = await readHexFile('./tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt'); + const input = await readHexFile( + './tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt', + ); const instance = new DlmsStreamParser({ stream, @@ -212,4 +248,4 @@ describe('Stream DLMS', () => { assert.equal(result.additionalAuthenticatedDataValid, false); }); }); -}); \ No newline at end of file +}); diff --git a/tests/stream/stream-dsmr.spec.ts b/tests/stream/stream-dsmr.spec.ts index c9a648d..87a506f 100644 --- a/tests/stream/stream-dsmr.spec.ts +++ b/tests/stream/stream-dsmr.spec.ts @@ -487,7 +487,9 @@ describe('DSMRStreamParser', () => { it('Throws an error if key is invalid', async () => { const stream = new PassThrough(); const callback = mock.fn(); - const { input } = await readDsmrTelegramFromFiles('./tests/telegrams/dsmr/dsmr-5.0-spec-example'); + const { input } = await readDsmrTelegramFromFiles( + './tests/telegrams/dsmr/dsmr-5.0-spec-example', + ); const encrypted = encryptFrame({ frame: input, key: TEST_DECRYPTION_KEY, aad: TEST_AAD }); const instance = new EncryptedDSMRStreamParser({ diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 21b0087..2a593d9 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -6,7 +6,13 @@ import { ENCRYPTED_DLMS_SYSTEM_TITLE_LEN, ENCRYPTED_DLMS_TELEGRAM_SOF, } from '../src/protocols/encryption.js'; -import { HDLC_TELEGRAM_SOF_EOF, HDLC_LLC_DESTINATION, HDLC_LLC_SOURCE, HDLC_LLC_QUALITY, HDLC_FORMAT_START } from '../src/protocols/hdlc.js'; +import { + HDLC_TELEGRAM_SOF_EOF, + HDLC_LLC_DESTINATION, + HDLC_LLC_SOURCE, + HDLC_LLC_QUALITY, + HDLC_FORMAT_START, +} from '../src/protocols/hdlc.js'; import { calculateCrc16IbmSdlc } from '../src/util/crc.js'; export const TEST_DECRYPTION_KEY = Buffer.from('0123456789abcdef01234567890abcdef', 'hex'); @@ -42,7 +48,7 @@ export const readDlmsTelegramFromFiles = async (path: string) => { input, output: JSON.parse(output.toString()) as object, }; -} +}; export const readHexFile = async (path: string) => { const file = await fs.readFile(path, 'utf-8'); @@ -114,38 +120,38 @@ export const wrapHdlcFrame = (frame: Buffer) => { HDLC_LLC_SOURCE, // 9: LLC Source HDLC_LLC_QUALITY, // 10: LLC Quality ]); - + const hdlcFooter = Buffer.from([ 0x00, // Checksum 0x00, // Checksum HDLC_TELEGRAM_SOF_EOF, ]); - + // Frame length is total length - 2 (SOF and EOF) const frameLength = frame.length + hdlcHeader.length + hdlcFooter.length - 2; - + if (frameLength > 0x7ff) { throw new Error('Frame length is too long to fit in HDLC'); } - + // Leave segmentation bit to 0. hdlcHeader[1] = (HDLC_FORMAT_START << 4) | ((frameLength >> 8) & 0x07); hdlcHeader[2] = frameLength & 0xff; - + // Don't include SOF in the checksum calculation const headerChecksum = calculateCrc16IbmSdlc(hdlcHeader.subarray(1, 6)); - + hdlcHeader.writeUint16LE(headerChecksum, 6); - + const frameUntilFooter = Buffer.concat([hdlcHeader, frame]); - + // Don't include SOF in the checksum calculation const footerChecksum = calculateCrc16IbmSdlc(frameUntilFooter.subarray(1)); - + hdlcFooter.writeUint16LE(footerChecksum, 0); - + return Buffer.concat([frameUntilFooter, hdlcFooter]); -} +}; export const encryptFrame = ({ frame, diff --git a/tools/parse-dlms.ts b/tools/parse-dlms.ts index d6e512b..93cbb54 100644 --- a/tools/parse-dlms.ts +++ b/tools/parse-dlms.ts @@ -6,6 +6,7 @@ import { bufferToHexString, numToHex, readHexFile } from '../tests/test-utils.js import { decodeDLMSContent, decodeDlmsObis } from '../src/protocols/dlms.js'; import { isDlmsStructureLike, ParsedDlmsData } from '../src/protocols/dlms-datatype.js'; import { obisCodeToString, parseObisCodeFromBuffer } from '../src/protocols/obis-code.js'; +import { HdlcParserResult } from '../src/protocols/hdlc.js'; const filePath = process.argv[2]; @@ -76,14 +77,49 @@ console.log(` - Timestamp: ${dlmsContent.timestamp.toString('hex')}`); console.log(` - DLMS Data:`); console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); -const energyContent = decodeDlmsObis(dlmsContent); -delete energyContent.hdlc; +const result: HdlcParserResult = { + hdlc: { + raw: '', + header: { + destinationAddress: 0, + sourceAddress: 0, + crc: { + value: 0, + valid: false, + }, + }, + crc: { + value: 0, + valid: false, + }, + }, + // DLMS properties will be filled in by `decodeDlmsObis` + dlms: { + invokeId: 0, + timestamp: '', + unknownObjects: [], + payloadType: '', + }, + cosem: { + unknownObjects: [], + knownObjects: [], + }, + electricity: {}, + mBus: {}, + metadata: {}, +}; + +decodeDlmsObis(dlmsContent, result); +// @ts-expect-error TS is not happy that we delete this property. +delete result.hdlc; console.log('Content Parsed:'); -if (energyContent.dlms?.unknownObjects) { +if (result.dlms?.unknownObjects) { console.log('Unknown Objects:'); - console.log(objectToList(energyContent.dlms?.unknownObjects, ' ')); + console.log(objectToList(result.dlms?.unknownObjects, ' ')); console.log('Parsed Objects:'); } -delete energyContent.dlms; -console.log(objectToList(energyContent, ' ')); + +// @ts-expect-error TS is not happy that we delete this property. +delete result.dlms; +console.log(objectToList(result, ' ')); diff --git a/tools/parse-hdlc.ts b/tools/parse-hdlc.ts index d7d9e80..e711e89 100644 --- a/tools/parse-hdlc.ts +++ b/tools/parse-hdlc.ts @@ -3,10 +3,10 @@ import path from 'node:path'; import { inspect } from 'node:util'; import { - decodeHdlcFooter, decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH, + HdlcParserResult, } from '../src/protocols/hdlc.js'; import { bufferToHexString, numToHex, readHexFile } from '../tests/test-utils.js'; import { decodeDLMSContent, decodeDlmsObis } from '../src/protocols/dlms.js'; @@ -98,18 +98,49 @@ console.log(` - Timestamp: ${dlmsContent.timestamp.toString('hex')}`); console.log(` - DLMS Data:`); console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); -const energyContent = decodeDlmsObis(dlmsContent); -delete energyContent.hdlc; +const result: HdlcParserResult = { + hdlc: { + raw: '', + header: { + destinationAddress: 0, + sourceAddress: 0, + crc: { + value: 0, + valid: false, + }, + }, + crc: { + value: 0, + valid: false, + }, + }, + // DLMS properties will be filled in by `decodeDlmsObis` + dlms: { + invokeId: 0, + timestamp: '', + unknownObjects: [], + payloadType: '', + }, + cosem: { + unknownObjects: [], + knownObjects: [], + }, + electricity: {}, + mBus: {}, + metadata: {}, +}; + +decodeDlmsObis(dlmsContent, result); +// @ts-expect-error TS is not happy that we delete this property. +delete result.hdlc; console.log('Content Parsed:'); -if (energyContent.dlms?.unknownObjects) { +if (result.dlms?.unknownObjects) { console.log('Unknown Objects:'); - console.log(objectToList(energyContent.dlms?.unknownObjects, ' ')); + console.log(objectToList(result.dlms?.unknownObjects, ' ')); console.log('Parsed Objects:'); } -delete energyContent.dlms; -console.log(objectToList(energyContent, ' ')); -const footer = decodeHdlcFooter(frame); -console.log('Footer:'); -console.log(` - CRC: ${numToHex(footer.crc)} (valid: ${footer.crcValid})`); +// @ts-expect-error TS is not happy that we delete this property. +delete result.dlms; +console.log(objectToList(result, ' ')); diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index a2ad0c4..fe64e65 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -23,7 +23,7 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr // Parse all DSMR telegrams { const testCases = await getAllDSMRTestTelegramTestCases(); - + for (const file of testCases) { let input = await fs.readFile(`./tests/telegrams/dsmr/${file}.txt`, 'utf-8'); input = input.replace(/\r?\n/g, '\r\n'); @@ -38,21 +38,21 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr { const fileToEncrypt = 'dsmr-luxembourgh-spec-example'; console.log(`Using ${fileToEncrypt} as test case for encrypted DSMR telegrams`); - + let input = await fs.readFile(`./tests/telegrams/dsmr/${fileToEncrypt}.txt`, 'utf-8'); input = input.replace(/\r?\n/g, '\r\n'); - + const encryptedAad = encryptFrame({ frame: Buffer.from(input, 'utf-8'), key: TEST_DECRYPTION_KEY, aad: TEST_AAD, }); - + await writeHexFile( `./tests/telegrams/dsmr/encrypted/${fileToEncrypt}-with-aad.txt`, encryptedAad, ); - + const encryptedWithoutAad = encryptFrame({ frame: Buffer.from(input, 'utf-8'), key: TEST_DECRYPTION_KEY, @@ -115,7 +115,7 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr { const dlmsFileToEncrypt = 'aidon-example-2'; console.log(`Using ${dlmsFileToEncrypt} as test case for encrypted DLMS telegrams`); - + const input = await readHexFile(`./tests/telegrams/dlms/${dlmsFileToEncrypt}.txt`); const hdlcHeader = decodeHdlcHeader(input); @@ -123,21 +123,21 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr const frame = input.subarray(0, hdlcHeader.frameLength + 2); const frameContent = frame.subarray(hdlcHeader.consumedBytes); - console.log(`frameContent`, frameContent.subarray(0, 10)) + console.log(`frameContent`, frameContent.subarray(0, 10)); const llc = decodeLlcHeader(frameContent); const content = frame.subarray( hdlcHeader.consumedBytes + llc.consumedBytes, frame.length - HDLC_FOOTER_LENGTH, ); - + const encryptedAad = encryptFrame({ frame: content, // frame: Buffer.from('Hello, world! 12345678901234567890123456789'), key: TEST_DECRYPTION_KEY, aad: TEST_AAD, }); - + const encryptedWithoutAad = encryptFrame({ frame: content, key: TEST_DECRYPTION_KEY, @@ -154,4 +154,4 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr `./tests/telegrams/dlms/encrypted/${dlmsFileToEncrypt}-without-aad.txt`, frameWithoutAad, ); -} \ No newline at end of file +} From 85c0abb06ac9577ce39f2da377c4fe403bc95257 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 27 May 2025 09:38:40 +0200 Subject: [PATCH 05/18] feat(hdlc): add support for segmented frames --- src/protocols/hdlc.ts | 18 ++- src/stream/stream-dlms.ts | 98 ++++++++------ tests/telegrams/dlms/aidon-example-1.json | 26 ++-- .../dlms/aidon-example-2-encrypted.json | 2 +- .../dlms/aidon-example-2-segmented.json | 124 ++++++++++++++++++ .../dlms/aidon-example-2-segmented.txt | 38 ++++++ tests/telegrams/dlms/aidon-example-2.json | 26 ++-- tests/telegrams/dlms/kamstrup-example-1.json | 26 ++-- tests/telegrams/dlms/kamstrup-example-2.json | 26 ++-- tests/telegrams/dlms/kamstrup-example-3.json | 26 ++-- tests/test-utils.ts | 13 +- tools/parse-telegram.ts | 6 +- tools/update-test-telegrams.ts | 12 +- tools/wrap-hdlc.ts | 14 +- 14 files changed, 344 insertions(+), 111 deletions(-) create mode 100644 tests/telegrams/dlms/aidon-example-2-segmented.json create mode 100644 tests/telegrams/dlms/aidon-example-2-segmented.txt diff --git a/src/protocols/hdlc.ts b/src/protocols/hdlc.ts index b2c1b1c..6147df3 100644 --- a/src/protocols/hdlc.ts +++ b/src/protocols/hdlc.ts @@ -57,17 +57,21 @@ import { SmartMeterError, SmartMeterUnknownMessageTypeError } from '../util/erro export type HdlcParserResult = BaseParserResult & { hdlc: { - raw: string; - header: { + headers: { destinationAddress: number; sourceAddress: number; - crc?: { + crc: { value: number; valid: boolean; }; - }; - crc?: { - value: number; + }[]; + footers: { + crc: { + value: number; + valid: boolean; + }; + }[]; + crc: { valid: boolean; }; }; @@ -190,7 +194,7 @@ export const decodeLlcHeader = (frameContent: Buffer) => { export const decodeHdlcFooter = (frame: Buffer) => { if (frame[frame.length - 1] !== HDLC_TELEGRAM_SOF_EOF) { - throw new SmartMeterError(`Invalid footer eof 0x${frame[frame.length].toString(16)}`); + throw new SmartMeterError(`Invalid footer eof 0x${frame[frame.length - 1].toString(16)}`); } const crc = frame.readUint16LE(frame.length - HDLC_FOOTER_LENGTH); diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index 8783c92..3a5830a 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -39,6 +39,8 @@ export class DlmsStreamParser implements SmartMeterStreamParser { private telegram = Buffer.alloc(0); private cachedContent = Buffer.alloc(0); private header: ReturnType | undefined = undefined; + private headers: ReturnType[] = []; + private footers: ReturnType[] = []; private readonly boundOnData = this.onData.bind(this); private readonly boundOnFullFrameRequiredTimeout = this.onFullFrameRequiredTimeout.bind(this); @@ -61,10 +63,14 @@ export class DlmsStreamParser implements SmartMeterStreamParser { return; } - this.fullFrameRequiredTimeout = setTimeout( - this.boundOnFullFrameRequiredTimeout, - this.fullFrameRequiredWithinMs, - ); + // The timeout can be already started when we're parsing + // segmented HDLC frames. + if (!this.fullFrameRequiredTimeout) { + this.fullFrameRequiredTimeout = setTimeout( + this.boundOnFullFrameRequiredTimeout, + this.fullFrameRequiredWithinMs, + ); + } this.telegram = data.subarray(sofIndex, data.length); this.hasStartOfFrame = true; } else { @@ -74,6 +80,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { if (this.header === undefined && this.telegram.length >= HDLC_HEADER_LENGTH) { try { this.header = decodeHdlcHeader(this.telegram); + this.headers.push(this.header); } catch (error) { this.clear(); @@ -83,6 +90,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { this.options.callback(error, undefined); + // TODO: This seems weird, I've just cleared the buffer so this.telegram should be empty... const remainingData = this.telegram.subarray(1, this.telegram.length); this.hasStartOfFrame = false; this.header = undefined; @@ -99,20 +107,25 @@ export class DlmsStreamParser implements SmartMeterStreamParser { // Wait for more data to decode the header if (!this.header) return; - const totalLength = this.header.frameLength + 2; // 2 bytes for the sof and eof. + + // +2 bytes for the sof and eof which are not included in the frame length field in the header + const totalLength = this.header.frameLength + 2; if (this.telegram.length < totalLength) { return; // Wait for more data } + // A complete HDLC frame is available now. + const fullHdlcFrame = this.telegram.subarray(0, totalLength); + const footer = decodeHdlcFooter(fullHdlcFrame); + this.footers.push(footer); + + const frameContent = this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH); + this.cachedContent = Buffer.concat([this.cachedContent, frameContent]); + + // If the frame is segmented, the content is split over multiple HDLC frames. if (this.header.segmentation) { // This frame is not complete yet, wait for more data. - // TODO: Parse the footer and check the crc. - this.cachedContent = Buffer.concat([ - this.cachedContent, - this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH), - ]); - const remainingData = this.telegram.subarray(totalLength, this.telegram.length); this.hasStartOfFrame = false; this.header = undefined; @@ -126,40 +139,51 @@ export class DlmsStreamParser implements SmartMeterStreamParser { return; } + // We now have the complete contents. We can parse the DLMS content. clearTimeout(this.fullFrameRequiredTimeout); + this.fullFrameRequiredTimeout = undefined; try { - const content = Buffer.concat([ - this.cachedContent, - this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH), - ]); // Last two bytes of content are the footer - - const llc = decodeLlcHeader(content); - - const completeTelegram = this.telegram.subarray(0, totalLength); - - const footer = decodeHdlcFooter(completeTelegram); + const llc = decodeLlcHeader(this.cachedContent); const dlmsContent = decodeDLMSContent({ - frame: content.subarray(llc.consumedBytes), + frame: this.cachedContent.subarray(llc.consumedBytes), decryptionKey: this.options.decryptionKey, additionalAuthenticatedData: this.options.additionalAuthenticatedData, }); + let allCrcValid = true; + const result: HdlcParserResult = { hdlc: { - raw: completeTelegram.toString('hex'), - header: { - destinationAddress: this.header.destinationAddress, - sourceAddress: this.header.sourceAddress, - crc: { - value: this.header.crc, - valid: this.header.crcValid, - }, - }, + headers: this.headers.map((header) => { + if (!header.crcValid) { + allCrcValid = false; + } + + return { + destinationAddress: header.destinationAddress, + sourceAddress: header.sourceAddress, + crc: { + valid: header.crcValid, + value: header.crc, + }, + }; + }), + footers: this.footers.map((footer) => { + if (!footer.crcValid) { + allCrcValid = false; + } + + return { + crc: { + valid: footer.crcValid, + value: footer.crc, + } + }; + }), crc: { - value: footer.crc, - valid: footer.crcValid, + valid: allCrcValid, }, }, // DLMS properties will be filled in by `decodeDlmsObis` @@ -194,10 +218,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { } const remainingData = this.telegram.subarray(totalLength, this.telegram.length); - this.hasStartOfFrame = false; - this.header = undefined; - this.telegram = Buffer.alloc(0); - this.cachedContent = Buffer.alloc(0); + this.clear(); // There might be more data in the buffer for the next telegram. if (remainingData.length > 0) { @@ -221,8 +242,11 @@ export class DlmsStreamParser implements SmartMeterStreamParser { clear(): void { clearTimeout(this.fullFrameRequiredTimeout); + this.fullFrameRequiredTimeout = undefined; this.hasStartOfFrame = false; this.header = undefined; + this.headers = []; + this.footers = []; this.telegram = Buffer.alloc(0); this.cachedContent = Buffer.alloc(0); } diff --git a/tests/telegrams/dlms/aidon-example-1.json b/tests/telegrams/dlms/aidon-example-1.json index 27ee5c0..4111a34 100644 --- a/tests/telegrams/dlms/aidon-example-1.json +++ b/tests/telegrams/dlms/aidon-example-1.json @@ -1,16 +1,24 @@ { "hdlc": { - "raw": "7ea0d24108831382d6e6e7000f40000000000109020209060101000281ff0a0b4149444f4e5f5630303031020209060000600100ff0a1037333539393932383930393431373432020209060000600107ff0a0436353135020309060100010700ff060000055202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000003e402020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10005d02020fff1621020309060100200700ff1209c402020fff1623e0c47e", - "header": { - "destinationAddress": 32, - "sourceAddress": 577, - "crc": { - "value": 54914, - "valid": true + "headers": [ + { + "destinationAddress": 32, + "sourceAddress": 577, + "crc": { + "valid": true, + "value": 54914 + } } - }, + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 50400 + } + } + ], "crc": { - "value": 50400, "valid": true } }, diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.json b/tests/telegrams/dlms/aidon-example-2-encrypted.json index e094f79..0ae588d 100644 --- a/tests/telegrams/dlms/aidon-example-2-encrypted.json +++ b/tests/telegrams/dlms/aidon-example-2-encrypted.json @@ -2,6 +2,6 @@ "error": { "message": "Encrypted frame detected", "name": "DecryptionRequired", - "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:59:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:141:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" + "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:58:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:146:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" } } \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-segmented.json b/tests/telegrams/dlms/aidon-example-2-segmented.json new file mode 100644 index 0000000..99f5a3c --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2-segmented.json @@ -0,0 +1,124 @@ +{ + "hdlc": { + "headers": [ + { + "destinationAddress": 1, + "sourceAddress": 2, + "crc": { + "valid": true, + "value": 33463 + } + }, + { + "destinationAddress": 1, + "sourceAddress": 2, + "crc": { + "valid": true, + "value": 33463 + } + }, + { + "destinationAddress": 1, + "sourceAddress": 2, + "crc": { + "valid": true, + "value": 50220 + } + } + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 60211 + } + }, + { + "crc": { + "valid": true, + "value": 34202 + } + }, + { + "crc": { + "valid": true, + "value": 11908 + } + } + ], + "crc": { + "valid": true + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [], + "payloadType": "BasicStructure" + }, + "cosem": { + "unknownObjects": [ + "1-0:3.7.0(1507*var)", + "1-0:4.7.0(0*var)", + "1-0:23.7.0(0*var)", + "1-0:24.7.0(0*var)", + "1-0:43.7.0(1506*var)", + "1-0:44.7.0(0*var)", + "1-0:63.7.0(0*var)", + "1-0:64.7.0(0*var)", + "1-0:3.8.0(6614347*varh)", + "1-0:4.8.0(5*varh)" + ], + "knownObjects": [ + "0-0:1.0.0(07e30c1001073b28ff8000ff)", + "1-0:1.7.0(1122*W)", + "1-0:2.7.0(0*W)", + "1-0:31.7.0(0*A)", + "1-0:51.7.0(7.5*A)", + "1-0:71.7.0(0*A)", + "1-0:32.7.0(230.70000000000002*V)", + "1-0:52.7.0(249.9*V)", + "1-0:72.7.0(230.8*V)", + "1-0:21.7.0(0*W)", + "1-0:22.7.0(0*W)", + "1-0:41.7.0(1122*W)", + "1-0:42.7.0(0*W)", + "1-0:61.7.0(0*W)", + "1-0:62.7.0(0*W)", + "1-0:1.8.0(10049926*Wh)", + "1-0:2.8.0(8*Wh)" + ] + }, + "electricity": { + "powerReceivedTotal": 1122000, + "powerReturnedTotal": 0, + "current": { + "l1": 0, + "l2": 7.5, + "l3": 0 + }, + "voltage": { + "l1": 230.70000000000002, + "l2": 249.9, + "l3": 230.8 + }, + "powerReceived": { + "l1": 0, + "l2": 1122, + "l3": 0 + }, + "powerReturned": { + "l1": 0, + "l2": 0, + "l3": 0 + }, + "total": { + "received": 10049926, + "returned": 8 + } + }, + "mBus": {}, + "metadata": { + "timestamp": "07e30c1001073b28ff8000ff" + } +} \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-segmented.txt b/tests/telegrams/dlms/aidon-example-2-segmented.txt new file mode 100644 index 0000000..ef5a7e7 --- /dev/null +++ b/tests/telegrams/dlms/aidon-example-2-segmented.txt @@ -0,0 +1,38 @@ +7e a8 c7 03 05 00 b7 82 e6 e7 00 0f 40 00 00 00 +00 01 1b 02 02 09 06 00 00 01 00 00 ff 09 0c 07 +e3 0c 10 01 07 3b 28 ff 80 00 ff 02 03 09 06 01 +00 01 07 00 ff 06 00 00 04 62 02 02 0f 00 16 1b +02 03 09 06 01 00 02 07 00 ff 06 00 00 00 00 02 +02 0f 00 16 1b 02 03 09 06 01 00 03 07 00 ff 06 +00 00 05 e3 02 02 0f 00 16 1d 02 03 09 06 01 00 +04 07 00 ff 06 00 00 00 00 02 02 0f 00 16 1d 02 +03 09 06 01 00 1f 07 00 ff 10 00 00 02 02 0f ff +16 21 02 03 09 06 01 00 33 07 00 ff 10 00 4b 02 +02 0f ff 16 21 02 03 09 06 01 00 47 07 00 ff 10 +00 00 02 02 0f ff 16 21 02 03 09 06 01 00 20 07 +00 ff 12 09 03 02 33 eb 7e 7e a8 c7 03 05 00 b7 +82 02 0f ff 16 23 02 03 09 06 01 00 34 07 00 ff +12 09 c3 02 02 0f ff 16 23 02 03 09 06 01 00 48 +07 00 ff 12 09 04 02 02 0f ff 16 23 02 03 09 06 +01 00 15 07 00 ff 06 00 00 00 00 02 02 0f 00 16 +1b 02 03 09 06 01 00 16 07 00 ff 06 00 00 00 00 +02 02 0f 00 16 1b 02 03 09 06 01 00 17 07 00 ff +06 00 00 00 00 02 02 0f 00 16 1d 02 03 09 06 01 +00 18 07 00 ff 06 00 00 00 00 02 02 0f 00 16 1d +02 03 09 06 01 00 29 07 00 ff 06 00 00 04 62 02 +02 0f 00 16 1b 02 03 09 06 01 00 2a 07 00 ff 06 +00 00 00 00 02 02 0f 00 16 1b 02 03 09 06 01 00 +2b 07 00 ff 06 00 00 05 e2 02 02 0f 00 16 1d 9a +85 7e 7e a0 c6 03 05 00 2c c4 02 03 09 06 01 00 +2c 07 00 ff 06 00 00 00 00 02 02 0f 00 16 1d 02 +03 09 06 01 00 3d 07 00 ff 06 00 00 00 00 02 02 +0f 00 16 1b 02 03 09 06 01 00 3e 07 00 ff 06 00 +00 00 00 02 02 0f 00 16 1b 02 03 09 06 01 00 3f +07 00 ff 06 00 00 00 00 02 02 0f 00 16 1d 02 03 +09 06 01 00 40 07 00 ff 06 00 00 00 00 02 02 0f +00 16 1d 02 03 09 06 01 00 01 08 00 ff 06 00 99 +59 86 02 02 0f 00 16 1e 02 03 09 06 01 00 02 08 +00 ff 06 00 00 00 08 02 02 0f 00 16 1e 02 03 09 +06 01 00 03 08 00 ff 06 00 64 ed 4b 02 02 0f 00 +16 20 02 03 09 06 01 00 04 08 00 ff 06 00 00 00 +05 02 02 0f 00 16 20 84 2e 7e diff --git a/tests/telegrams/dlms/aidon-example-2.json b/tests/telegrams/dlms/aidon-example-2.json index 21a3755..d015740 100644 --- a/tests/telegrams/dlms/aidon-example-2.json +++ b/tests/telegrams/dlms/aidon-example-2.json @@ -1,16 +1,24 @@ { "hdlc": { - "raw": "7ea2434108831385ebe6e7000f4000000000011b020209060000010000ff090c07e30c1001073b28ff8000ff020309060100010700ff060000046202020f00161b020309060100020700ff060000000002020f00161b020309060100030700ff06000005e302020f00161d020309060100040700ff060000000002020f00161d0203090601001f0700ff10000002020fff1621020309060100330700ff10004b02020fff1621020309060100470700ff10000002020fff1621020309060100200700ff12090302020fff1623020309060100340700ff1209c302020fff1623020309060100480700ff12090402020fff1623020309060100150700ff060000000002020f00161b020309060100160700ff060000000002020f00161b020309060100170700ff060000000002020f00161d020309060100180700ff060000000002020f00161d020309060100290700ff060000046202020f00161b0203090601002a0700ff060000000002020f00161b0203090601002b0700ff06000005e202020f00161d0203090601002c0700ff060000000002020f00161d0203090601003d0700ff060000000002020f00161b0203090601003e0700ff060000000002020f00161b0203090601003f0700ff060000000002020f00161d020309060100400700ff060000000002020f00161d020309060100010800ff060099598602020f00161e020309060100020800ff060000000802020f00161e020309060100030800ff060064ed4b02020f001620020309060100040800ff060000000502020f001620be407e", - "header": { - "destinationAddress": 32, - "sourceAddress": 577, - "crc": { - "value": 60293, - "valid": true + "headers": [ + { + "destinationAddress": 32, + "sourceAddress": 577, + "crc": { + "valid": true, + "value": 60293 + } } - }, + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 16574 + } + } + ], "crc": { - "value": 16574, "valid": true } }, diff --git a/tests/telegrams/dlms/kamstrup-example-1.json b/tests/telegrams/dlms/kamstrup-example-1.json index 2252049..113ac54 100644 --- a/tests/telegrams/dlms/kamstrup-example-1.json +++ b/tests/telegrams/dlms/kamstrup-example-1.json @@ -1,16 +1,24 @@ { "hdlc": { - "raw": "7ea0e22b2113239ae6e7000f000000000c07d0010106162100ff80000102190a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff1200005be57e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 39459, - "valid": true + "headers": [ + { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "valid": true, + "value": 39459 + } } - }, + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 58715 + } + } + ], "crc": { - "value": 58715, "valid": true } }, diff --git a/tests/telegrams/dlms/kamstrup-example-2.json b/tests/telegrams/dlms/kamstrup-example-2.json index 3dde605..e8b6ab7 100644 --- a/tests/telegrams/dlms/kamstrup-example-2.json +++ b/tests/telegrams/dlms/kamstrup-example-2.json @@ -1,16 +1,24 @@ { "hdlc": { - "raw": "7ea12c2b2113fc04e6e7000f000000000c07e1081003100005ff80000002230a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff060000000009060101020700ff060000000009060101030700ff060000000009060101040700ff0600000000090601011f0700ff060000000009060101330700ff060000000009060101470700ff060000000009060101200700ff12000009060101340700ff12000009060101480700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000009060101020800ff060000000009060101030800ff060000000009060101040800ff0600000000c8867e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 1276, - "valid": true + "headers": [ + { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "valid": true, + "value": 1276 + } } - }, + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 34504 + } + } + ], "crc": { - "value": 34504, "valid": true } }, diff --git a/tests/telegrams/dlms/kamstrup-example-3.json b/tests/telegrams/dlms/kamstrup-example-3.json index 49ee689..9bcc8eb 100644 --- a/tests/telegrams/dlms/kamstrup-example-3.json +++ b/tests/telegrams/dlms/kamstrup-example-3.json @@ -1,16 +1,24 @@ { "hdlc": { - "raw": "7ea0ae2b2113a01be6e7000f000000000c07e1081003100005ff800000020f0a0e4b616d73747275705f563030303109060101000005ff0a103537303635363730303030303030303009060101600101ff0a1230303030303030303030303030303030303009060101010700ff0600000000090601011f0700ff060000000009060101200700ff12000009060001010000ff090c07e1081003100005ff80000009060101010800ff060000000005217e", - "header": { - "destinationAddress": 21, - "sourceAddress": 16, - "crc": { - "value": 7072, - "valid": true + "headers": [ + { + "destinationAddress": 21, + "sourceAddress": 16, + "crc": { + "valid": true, + "value": 7072 + } } - }, + ], + "footers": [ + { + "crc": { + "valid": true, + "value": 8453 + } + } + ], "crc": { - "value": 8453, "valid": true } }, diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 2a593d9..b8689c1 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -8,9 +8,6 @@ import { } from '../src/protocols/encryption.js'; import { HDLC_TELEGRAM_SOF_EOF, - HDLC_LLC_DESTINATION, - HDLC_LLC_SOURCE, - HDLC_LLC_QUALITY, HDLC_FORMAT_START, } from '../src/protocols/hdlc.js'; import { calculateCrc16IbmSdlc } from '../src/util/crc.js'; @@ -106,7 +103,7 @@ export const bufferToHexString = (buffer: Buffer) => { return hexString; }; -export const wrapHdlcFrame = (frame: Buffer) => { +export const wrapHdlcFrame = (frame: Buffer, isSegmented = false) => { const hdlcHeader = Buffer.from([ HDLC_TELEGRAM_SOF_EOF, // 0: SOF 0x00, // 1: Format type + length @@ -116,9 +113,6 @@ export const wrapHdlcFrame = (frame: Buffer) => { 0x00, // 5: Control byte, 0x00, // 6: Checksum 0x00, // 7: Checksum, - HDLC_LLC_DESTINATION, // 8: LLC Destination - HDLC_LLC_SOURCE, // 9: LLC Source - HDLC_LLC_QUALITY, // 10: LLC Quality ]); const hdlcFooter = Buffer.from([ @@ -134,10 +128,13 @@ export const wrapHdlcFrame = (frame: Buffer) => { throw new Error('Frame length is too long to fit in HDLC'); } - // Leave segmentation bit to 0. hdlcHeader[1] = (HDLC_FORMAT_START << 4) | ((frameLength >> 8) & 0x07); hdlcHeader[2] = frameLength & 0xff; + if (isSegmented) { + hdlcHeader[1] |= 0x08; // Set segmentation bit + } + // Don't include SOF in the checksum calculation const headerChecksum = calculateCrc16IbmSdlc(hdlcHeader.subarray(1, 6)); diff --git a/tools/parse-telegram.ts b/tools/parse-telegram.ts index 455da48..59747b1 100644 --- a/tools/parse-telegram.ts +++ b/tools/parse-telegram.ts @@ -84,11 +84,9 @@ const callback: SmartMeterStreamCallback = (error, result) => { } else { let crcValid = true; if ('hdlc' in result) { - const msgCrcValid = result.hdlc.crc?.valid === false; - const hdrCrcValid = result.hdlc.header.crc?.valid === false; - crcValid = msgCrcValid && hdrCrcValid; + crcValid = result.hdlc.crc.valid !== false; } else if ('dsmr' in result) { - crcValid = result.dsmr.crc?.valid === false; + crcValid = result.dsmr.crc?.valid !== false; } if (!crcValid) { diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index fe64e65..8b911f8 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -123,8 +123,6 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr const frame = input.subarray(0, hdlcHeader.frameLength + 2); const frameContent = frame.subarray(hdlcHeader.consumedBytes); - console.log(`frameContent`, frameContent.subarray(0, 10)); - const llc = decodeLlcHeader(frameContent); const content = frame.subarray( hdlcHeader.consumedBytes + llc.consumedBytes, @@ -143,8 +141,14 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr key: TEST_DECRYPTION_KEY, }); - const frameWithAad = wrapHdlcFrame(encryptedAad); - const frameWithoutAad = wrapHdlcFrame(encryptedWithoutAad); + const llcBuffer = Buffer.from([ + llc.destination, + llc.source, + llc.quality + ]); + + const frameWithAad = wrapHdlcFrame(Buffer.concat([llcBuffer, encryptedAad])); + const frameWithoutAad = wrapHdlcFrame(Buffer.concat([llcBuffer, encryptedWithoutAad])); await writeHexFile( `./tests/telegrams/dlms/encrypted/${dlmsFileToEncrypt}-with-aad.txt`, diff --git a/tools/wrap-hdlc.ts b/tools/wrap-hdlc.ts index 97fba8a..5fd8ea9 100644 --- a/tools/wrap-hdlc.ts +++ b/tools/wrap-hdlc.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ /** Wraps a hex-encoded binary file in a HDLC frame. */ import path from 'node:path'; -import { readHexFile, wrapHdlcFrame, writeHexFile } from '../tests/test-utils.js'; +import { chunkBuffer, readHexFile, wrapHdlcFrame, writeHexFile } from '../tests/test-utils.js'; const srcPath = process.argv[2]; const dstPath = process.argv[3]; @@ -9,19 +9,23 @@ const dstPath = process.argv[3]; if (srcPath === undefined || dstPath === undefined) { console.error('Please provide a file path as argument.'); console.log('Usage:'); - console.log('npm run tool:wrap-hdlc '); + console.log('npm run tool:wrap-hdlc []'); process.exit(1); } const srcPathResolved = path.resolve(process.cwd(), srcPath); const dstPathResolved = path.resolve(process.cwd(), dstPath); +const numberOfSegments = parseInt(process.argv[4], 10) || 1; const srcFile = await readHexFile(srcPathResolved); -const hdlcFrame = wrapHdlcFrame(srcFile); +const chunks = chunkBuffer(srcFile, Math.ceil(srcFile.length / numberOfSegments)); -const hexString = await writeHexFile(dstPathResolved, hdlcFrame); +const hdlcFrames = chunks.map((chunk, index) => wrapHdlcFrame(chunk, index < chunks.length - 1)); +const allData = Buffer.concat(hdlcFrames); + +const hexString = await writeHexFile(dstPathResolved, allData); console.log('File written to', dstPathResolved); -console.log('File size:', hdlcFrame.length); +console.log('File size:', allData.length); console.log('File content:'); console.log(hexString); From 0c9d0c4cbeec4b8fd07e14d9717e024791c3ffba Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 27 May 2025 10:41:14 +0200 Subject: [PATCH 06/18] feat: move raw data out of result objects --- src/protocols/dsmr.ts | 2 - src/stream/stream-dlms.ts | 34 ++++++---- src/stream/stream-encrypted-dsmr.ts | 24 +++---- src/stream/stream-unencrypted-dsmr.ts | 13 ++-- src/stream/stream.ts | 3 +- src/util/errors.ts | 10 +++ tests/protocols/dsmr.spec.ts | 3 - tests/stream/stream-detect-type.spec.ts | 8 ++- tests/stream/stream-dlms.spec.ts | 64 ++++++++++++++++++- tests/stream/stream-dsmr.spec.ts | 34 ++++++++-- .../dlms/aidon-example-2-encrypted.json | 7 -- .../dlms/aidon-example-2-encrypted.txt | 41 ------------ tests/telegrams/dsmr/dsmr-2.2-kfm-1.json | 1 - .../telegrams/dsmr/dsmr-3.0-spec-example.json | 1 - tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 1 - tests/telegrams/dsmr/dsmr-4.0-isk-2.json | 1 - .../telegrams/dsmr/dsmr-4.0-spec-example.json | 1 - tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 1 - tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 1 - .../dsmr/dsmr-4.2.2-spec-example.json | 1 - tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 1 - tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 1 - tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 1 - tests/telegrams/dsmr/dsmr-5.0-est-units.json | 1 - tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 1 - .../dsmr/dsmr-5.0-spec-example-lowercase.json | 1 - .../telegrams/dsmr/dsmr-5.0-spec-example.json | 1 - .../dsmr/dsmr-luxembourgh-spec-example.json | 1 - .../dsmr/emucs-p1-v2.1.1-spec-example-1.json | 1 - .../dsmr/emucs-p1-v2.1.1-spec-example-2.json | 1 - ...iskra-mt-382-no-crc-with-text-message.json | 1 - tests/telegrams/dsmr/iskra-mt-382-no-crc.json | 1 - .../kamstrup-OMNIA-e-meter-three-phase.json | 1 - tests/telegrams/dsmr/sagemcom-xt211.json | 1 - tests/telegrams/dsmr/unknown-xmx-1.json | 1 - tests/test-utils.ts | 5 +- tools/parse-dlms.ts | 26 +++++--- tools/parse-hdlc.ts | 26 +++++--- tools/update-test-telegrams.ts | 26 +++----- 39 files changed, 196 insertions(+), 153 deletions(-) delete mode 100644 tests/telegrams/dlms/aidon-example-2-encrypted.json delete mode 100644 tests/telegrams/dlms/aidon-example-2-encrypted.txt diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts index 6a68611..4db7673 100644 --- a/src/protocols/dsmr.ts +++ b/src/protocols/dsmr.ts @@ -27,7 +27,6 @@ export type DsmrParserOptions = export type DsmrParserResult = BaseParserResult & { dsmr: { - raw: string; header: { identifier: string; xxx: string; @@ -196,7 +195,6 @@ export const parseDsmr = (options: DsmrParserOptions): DsmrParserResult => { const result: DsmrParserResult = { dsmr: { - raw: telegram, header: { identifier: '', xxx: '', diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index 3a5830a..200f542 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -12,6 +12,7 @@ import { SmartMeterError, SmartMeterTimeoutError, StartOfFrameNotFoundError, + toSmartMeterError, } from '../util/errors.js'; import { decodeDLMSContent, decodeDlmsObis } from './../protocols/dlms.js'; import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; @@ -41,6 +42,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { private header: ReturnType | undefined = undefined; private headers: ReturnType[] = []; private footers: ReturnType[] = []; + private telegrams: Buffer[] = []; private readonly boundOnData = this.onData.bind(this); private readonly boundOnFullFrameRequiredTimeout = this.onFullFrameRequiredTimeout.bind(this); @@ -59,7 +61,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { const error = new StartOfFrameNotFoundError(); error.withRawTelegram(data); - this.options.callback(error, undefined); + this.options.callback(error); return; } @@ -81,14 +83,16 @@ export class DlmsStreamParser implements SmartMeterStreamParser { try { this.header = decodeHdlcHeader(this.telegram); this.headers.push(this.header); - } catch (error) { + } catch (rawError) { this.clear(); + const error = toSmartMeterError(rawError); + if (error instanceof SmartMeterError) { error.withRawTelegram(this.telegram); } - this.options.callback(error, undefined); + this.options.callback(error); // TODO: This seems weird, I've just cleared the buffer so this.telegram should be empty... const remainingData = this.telegram.subarray(1, this.telegram.length); @@ -107,7 +111,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { // Wait for more data to decode the header if (!this.header) return; - + // +2 bytes for the sof and eof which are not included in the frame length field in the header const totalLength = this.header.frameLength + 2; @@ -120,9 +124,15 @@ export class DlmsStreamParser implements SmartMeterStreamParser { const footer = decodeHdlcFooter(fullHdlcFrame); this.footers.push(footer); - const frameContent = this.telegram.subarray(this.header.consumedBytes, totalLength - HDLC_FOOTER_LENGTH); + const frameContent = this.telegram.subarray( + this.header.consumedBytes, + totalLength - HDLC_FOOTER_LENGTH, + ); this.cachedContent = Buffer.concat([this.cachedContent, frameContent]); + const telegram = this.telegram.subarray(0, totalLength); + this.telegrams.push(telegram); + // If the frame is segmented, the content is split over multiple HDLC frames. if (this.header.segmentation) { // This frame is not complete yet, wait for more data. @@ -179,7 +189,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { crc: { valid: footer.crcValid, value: footer.crc, - } + }, }; }), crc: { @@ -208,13 +218,14 @@ export class DlmsStreamParser implements SmartMeterStreamParser { decodeDlmsObis(dlmsContent, result); - this.options.callback(null, result); - } catch (error) { + this.options.callback(null, result, Buffer.concat(this.telegrams)); + } catch (rawError) { + const error = toSmartMeterError(rawError); if (error instanceof SmartMeterError) { error.withRawTelegram(this.telegram); } - this.options.callback(error, undefined); + this.options.callback(error); } const remainingData = this.telegram.subarray(totalLength, this.telegram.length); @@ -229,7 +240,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { private onFullFrameRequiredTimeout() { const error = new SmartMeterTimeoutError(); error.withRawTelegram(this.telegram); - this.options.callback(error, undefined); + this.options.callback(error); // Reset the entire state here, as the full frame was not received. this.clear(); @@ -247,11 +258,12 @@ export class DlmsStreamParser implements SmartMeterStreamParser { this.header = undefined; this.headers = []; this.footers = []; + this.telegrams = []; this.telegram = Buffer.alloc(0); this.cachedContent = Buffer.alloc(0); } currentSize(): number { - return this.telegram.length + this.cachedContent.length; + return this.telegram.length + this.telegrams.reduce((acc, t) => acc + t.length, 0); } } diff --git a/src/stream/stream-encrypted-dsmr.ts b/src/stream/stream-encrypted-dsmr.ts index 915f840..a20790f 100644 --- a/src/stream/stream-encrypted-dsmr.ts +++ b/src/stream/stream-encrypted-dsmr.ts @@ -12,6 +12,7 @@ import { SmartMeterError, StartOfFrameNotFoundError, SmartMeterTimeoutError, + toSmartMeterError, } from '../util/errors.js'; import { SmartMeterStreamCallback, SmartMeterStreamParser } from './stream.js'; @@ -55,7 +56,7 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { const error = new StartOfFrameNotFoundError(); error.withRawTelegram(data); - this.options.callback(error, undefined); + this.options.callback(error); return; } @@ -72,14 +73,12 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { if (this.header === undefined && this.telegram.length >= ENCRYPTED_DLMS_HEADER_LEN) { try { this.header = decodeEncryptionHeader(this.telegram); - } catch (error) { + } catch (err) { this.clear(); + const error = toSmartMeterError(err); + error.withRawTelegram(this.telegram); - if (error instanceof SmartMeterError) { - error.withRawTelegram(this.telegram); - } - - this.options.callback(error, undefined); + this.options.callback(error); return; } } @@ -98,7 +97,8 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { let decryptError: Error | undefined; try { - const encryptedContent = this.telegram.subarray( + const telegram = this.telegram.subarray(0, totalLength); + const encryptedContent = telegram.subarray( ENCRYPTED_DLMS_HEADER_LEN, ENCRYPTED_DLMS_HEADER_LEN + this.header.contentLength, ); @@ -120,17 +120,17 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { result.additionalAuthenticatedDataValid = decryptError === undefined; - this.options.callback(null, result); + this.options.callback(null, result, telegram); } catch (error) { // If we had a decryption error that is the cause of the error. // So that should be returned to the listener. - const realError = decryptError ?? error; + const realError = decryptError ?? toSmartMeterError(error); if (realError instanceof SmartMeterError) { realError.withRawTelegram(this.telegram); } - this.options.callback(realError, undefined); + this.options.callback(realError); } const remainingData = this.telegram.subarray(totalLength, this.telegram.length); @@ -148,7 +148,7 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { private onFullFrameRequiredTimeout() { const error = new SmartMeterTimeoutError(); error.withRawTelegram(this.telegram); - this.options.callback(error, undefined); + this.options.callback(error); // Reset the entire state here, as the full frame was not received. this.clear(); diff --git a/src/stream/stream-unencrypted-dsmr.ts b/src/stream/stream-unencrypted-dsmr.ts index b670de5..d84b979 100644 --- a/src/stream/stream-unencrypted-dsmr.ts +++ b/src/stream/stream-unencrypted-dsmr.ts @@ -4,6 +4,7 @@ import { SmartMeterError, StartOfFrameNotFoundError, SmartMeterTimeoutError, + toSmartMeterError, } from '../util/errors.js'; import { SmartMeterStreamParser } from './stream.js'; @@ -42,7 +43,7 @@ export class UnencryptedDSMRStreamParser implements SmartMeterStreamParser { if (sofIndex === -1) { const error = new StartOfFrameNotFoundError(); error.withRawTelegram(this.telegram); - this.options.callback(error, undefined); + this.options.callback(error); this.telegram = Buffer.alloc(0); return; } @@ -97,18 +98,20 @@ export class UnencryptedDSMRStreamParser implements SmartMeterStreamParser { clearTimeout(this.fullFrameRequiredTimeout); try { + const telegram = this.telegram.subarray(0, frameLength); const result = parseDsmr({ - telegram: this.telegram.subarray(0, frameLength), + telegram, }); - this.options.callback(null, result); + this.options.callback(null, result, telegram); } catch (err) { - const error = overrideError ?? err; + const error = overrideError ?? toSmartMeterError(err); + if (error instanceof SmartMeterError) { error.withRawTelegram(this.telegram); } - this.options.callback(error, undefined); + this.options.callback(error); } const remainingData = this.telegram.subarray(frameLength, this.telegram.length); diff --git a/src/stream/stream.ts b/src/stream/stream.ts index 70452a3..0f6a053 100644 --- a/src/stream/stream.ts +++ b/src/stream/stream.ts @@ -13,4 +13,5 @@ export type SmartMeterStreamParser = { export type SmartMeterStreamCallback< TResult extends SmartMeterParserResult = SmartMeterParserResult, -> = (error: unknown, result?: TResult) => void; +> = ((error: null, result: TResult, rawData: Buffer) => void) & + ((error: Error, result?: undefined, rawData?: undefined) => void); diff --git a/src/util/errors.ts b/src/util/errors.ts index 885425d..94e3362 100644 --- a/src/util/errors.ts +++ b/src/util/errors.ts @@ -63,3 +63,13 @@ export class SmartMeterUnknownMessageTypeError extends SmartMeterError { this.name = 'UnknownMessageTypeError'; } } + +export const toSmartMeterError = (error: unknown) => { + if (error instanceof SmartMeterError) { + return error; + } else if (error instanceof Error) { + return new SmartMeterError(error.message, { cause: error }); + } else { + return new SmartMeterError(`Unknown error: ${String(error)}`); + } +}; diff --git a/tests/protocols/dsmr.spec.ts b/tests/protocols/dsmr.spec.ts index f026184..3f0d199 100644 --- a/tests/protocols/dsmr.spec.ts +++ b/tests/protocols/dsmr.spec.ts @@ -21,9 +21,6 @@ describe('DSMR', async () => { const parsed = parseDsmr({ telegram: input }); - // @ts-expect-error output is not typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - assert.equal(parsed.dsmr.raw, expectedOutput.dsmr.raw); assert.deepStrictEqual(JSON.parse(JSON.stringify(parsed)), expectedOutput); }); diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts index 5d95665..7c0eb9d 100644 --- a/tests/stream/stream-detect-type.spec.ts +++ b/tests/stream/stream-detect-type.spec.ts @@ -152,7 +152,9 @@ describe('Stream: Detect Type', () => { }); it('Detects encrypted DLMS telegrams', async () => { - const input = await readHexFile('./tests/telegrams/dlms/aidon-example-2-encrypted.txt'); + const input = await readHexFile( + './tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt', + ); const stream = new PassThrough(); const callback = mock.fn(); @@ -171,7 +173,9 @@ describe('Stream: Detect Type', () => { }); it('Detects encrypted DLMS telegrams (chunks)', async () => { - const input = await readHexFile('./tests/telegrams/dlms/aidon-example-2-encrypted.txt'); + const input = await readHexFile( + './tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt', + ); const stream = new PassThrough(); const callback = mock.fn(); diff --git a/tests/stream/stream-dlms.spec.ts b/tests/stream/stream-dlms.spec.ts index 8a5566a..3ec4660 100644 --- a/tests/stream/stream-dlms.spec.ts +++ b/tests/stream/stream-dlms.spec.ts @@ -3,6 +3,7 @@ import { describe, it, mock } from 'node:test'; import { chunkBuffer, + getAllDLMSTestTelegramTestCases, readDlmsTelegramFromFiles, readHexFile, TEST_AAD, @@ -21,7 +22,7 @@ import { HdlcParserResult, } from '../../src/protocols/hdlc.js'; -describe('Stream DLMS', () => { +describe('Stream DLMS', async () => { const testDlmsStreamParser = (input: Buffer) => { const stream = new PassThrough(); const callback = mock.fn(); @@ -38,6 +39,21 @@ describe('Stream DLMS', () => { return callback.mock.calls; }; + for (const testCase of await getAllDLMSTestTelegramTestCases()) { + it(`Parses ${testCase}`, async () => { + const { input, output: expectedOutput } = await readDlmsTelegramFromFiles( + `./tests/telegrams/dlms/${testCase}`, + ); + + const calls = testDlmsStreamParser(input); + + assert.deepStrictEqual(calls.length, 1); + assert.deepStrictEqual(calls[0].arguments[0], null); + assert.deepStrictEqual(calls[0].arguments[1], expectedOutput); + assert.deepStrictEqual(calls[0].arguments[2], input); + }); + } + describe('Unencrypted', () => { it('Parses a chunked unencrypted telegram', async () => { const { input, output } = await readDlmsTelegramFromFiles( @@ -66,6 +82,39 @@ describe('Stream DLMS', () => { callback.mock.calls[0].arguments[1], JSON.parse(JSON.stringify(output)), ); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], input); + }); + + // This case is different from the previous one, as the telegram is segmented into + // multiple HDLC frames. + it('Parses a chunked & segmented unencrypted telegram', async () => { + const { input, output } = await readDlmsTelegramFromFiles( + './tests/telegrams/dlms/aidon-example-2-segmented', + ); + + const chunks = chunkBuffer(input, 10); + const stream = new PassThrough(); + const callback = mock.fn(); + + const instance = new DlmsStreamParser({ + stream, + callback, + }); + + for (const chunk of chunks) { + stream.write(chunk); + } + + stream.end(); + instance.destroy(); + + assert.deepStrictEqual(callback.mock.calls.length, 1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); + assert.deepStrictEqual( + callback.mock.calls[0].arguments[1], + JSON.parse(JSON.stringify(output)), + ); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], input); }); it('Parses two unencrypted telegrams', async () => { @@ -81,8 +130,10 @@ describe('Stream DLMS', () => { assert.deepStrictEqual(calls.length, 2); assert.deepStrictEqual(calls[0].arguments[0], null); assert.deepStrictEqual(calls[0].arguments[1], output1); + assert.deepStrictEqual(calls[0].arguments[2], input1); assert.deepStrictEqual(calls[1].arguments[0], null); assert.deepStrictEqual(calls[1].arguments[1], output2); + assert.deepStrictEqual(calls[1].arguments[2], input2); }); it('Throws error when telegram is invalid', async () => { @@ -94,6 +145,7 @@ describe('Stream DLMS', () => { assert.equal(calls.length, 1); assert.ok(calls[0].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(calls[0].arguments[1], undefined); + assert.equal(calls[0].arguments[2], undefined); }); it('Throws error if a full frame is not received in time', async (context) => { @@ -128,6 +180,7 @@ describe('Stream DLMS', () => { assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); // Writing invalid data should now throw an error. stream.write('invalid data'); @@ -137,6 +190,8 @@ describe('Stream DLMS', () => { assert.equal(callback.mock.calls.length, 2); assert.ok(callback.mock.calls[1].arguments[0] instanceof StartOfFrameNotFoundError); + assert.equal(callback.mock.calls[1].arguments[1], undefined); + assert.equal(callback.mock.calls[1].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -163,6 +218,8 @@ describe('Stream DLMS', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -192,6 +249,7 @@ describe('Stream DLMS', () => { assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterDecryptionError); assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); }); it('Parses when AAD is invalid', async () => { @@ -219,6 +277,8 @@ describe('Stream DLMS', () => { const result = callback.mock.calls[0].arguments[1] as HdlcParserResult; assert.equal(result.additionalAuthenticatedDataValid, false); + + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], input); }); it('Parses when AAD is missing', async () => { @@ -246,6 +306,8 @@ describe('Stream DLMS', () => { const result = callback.mock.calls[0].arguments[1] as HdlcParserResult; assert.equal(result.additionalAuthenticatedDataValid, false); + + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], input); }); }); }); diff --git a/tests/stream/stream-dsmr.spec.ts b/tests/stream/stream-dsmr.spec.ts index 87a506f..3cb1717 100644 --- a/tests/stream/stream-dsmr.spec.ts +++ b/tests/stream/stream-dsmr.spec.ts @@ -64,6 +64,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); }); it('Parses two unencrypted telegrams', async () => { @@ -87,8 +88,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output1); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input1)); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output2); + assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input2)); }); it('Throws error when telegram is invalid', async () => { @@ -109,6 +112,7 @@ describe('DSMRStreamParser', () => { assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); }); it("Doesn't throw error after receiving null character", async () => { @@ -127,12 +131,8 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - - // Need to manually add \0 to the output - // @ts-expect-error output is not typed - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - output.dsmr.raw += '\0'; assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input + '\0')); }); it('Throws an error if a full frame is not received in time', async (context) => { @@ -164,6 +164,8 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); assert.equal(instance.currentSize(), 0); // Writing more data should trigger the sof error again. @@ -173,6 +175,8 @@ describe('DSMRStreamParser', () => { assert.equal(callback.mock.calls.length, 2); assert.ok(callback.mock.calls[1].arguments[0] instanceof StartOfFrameNotFoundError); + assert.equal(callback.mock.calls[1].arguments[1], undefined); + assert.equal(callback.mock.calls[1].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -199,6 +203,8 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -230,6 +236,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); }); it('Immediately parses when CRC is missing and a 2nd telegram is received', async (context) => { @@ -254,6 +261,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); context.mock.timers.tick(1000); @@ -263,6 +271,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input)); }); it('Immediately parses when CRC is missing and a three telegrams are received', async (context) => { @@ -288,8 +297,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input)); context.mock.timers.tick(1000); @@ -299,6 +310,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 3); assert.deepStrictEqual(callback.mock.calls[2].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[2].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[2].arguments[2], Buffer.from(input)); }); it('Handles text messages', async (context) => { @@ -324,6 +336,7 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); stream.end(); instance.destroy(); @@ -361,6 +374,7 @@ describe('DSMRStreamParser', () => { expected: output, aadValid: true, }); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], encrypted); }); it('Parses two encrypted telegrams', async () => { @@ -396,12 +410,14 @@ describe('DSMRStreamParser', () => { expected: output1, aadValid: true, }); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], encrypted1); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); assertDecryptedFrameValid({ actual: callback.mock.calls[1].arguments[1], expected: output2, aadValid: true, }); + assert.deepStrictEqual(callback.mock.calls[1].arguments[2], encrypted2); }); it('Throws error when telegram is invalid', async () => { @@ -423,6 +439,7 @@ describe('DSMRStreamParser', () => { assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof StartOfFrameNotFoundError); assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); }); it('Throws an error if a full frame is not received in time', async (context) => { @@ -451,6 +468,8 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -479,6 +498,8 @@ describe('DSMRStreamParser', () => { // Here it should have timed out and called the callback with an error. assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterTimeoutError); + assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); assert.equal(instance.currentSize(), 0); instance.destroy(); @@ -507,6 +528,7 @@ describe('DSMRStreamParser', () => { assert.equal(callback.mock.calls.length, 1); assert.ok(callback.mock.calls[0].arguments[0] instanceof SmartMeterDecryptionError); assert.equal(callback.mock.calls[0].arguments[1], undefined); + assert.equal(callback.mock.calls[0].arguments[2], undefined); }); it('Parses when AAD is invalid', async () => { @@ -537,6 +559,7 @@ describe('DSMRStreamParser', () => { expected: output, aadValid: false, }); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], encrypted); }); it('Parses when AAD is missing', async () => { @@ -567,6 +590,7 @@ describe('DSMRStreamParser', () => { expected: output, aadValid: false, }); + assert.deepStrictEqual(callback.mock.calls[0].arguments[2], encrypted); }); }); }); diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.json b/tests/telegrams/dlms/aidon-example-2-encrypted.json deleted file mode 100644 index 0ae588d..0000000 --- a/tests/telegrams/dlms/aidon-example-2-encrypted.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error": { - "message": "Encrypted frame detected", - "name": "DecryptionRequired", - "stack": "DecryptionRequired: Encrypted frame detected\n at decodeDLMSContent (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/protocols/dlms.ts:58:13)\n at DlmsStreamParser.onData (/Users/niels/projects/energy-dongle/node-dsmr-parser/src/stream/stream-dlms.ts:146:27)\n at PassThrough.emit (node:events:517:28)\n at addChunk (node:internal/streams/readable:368:12)\n at readableAddChunk (node:internal/streams/readable:341:9)\n at Readable.push (node:internal/streams/readable:278:10)\n at node:internal/streams/transform:182:12\n at PassThrough._transform (node:internal/streams/passthrough:46:3)\n at Transform._write (node:internal/streams/transform:175:8)\n at writeOrBuffer (node:internal/streams/writable:392:12)" - } -} \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-encrypted.txt b/tests/telegrams/dlms/aidon-example-2-encrypted.txt deleted file mode 100644 index ebb2d9c..0000000 --- a/tests/telegrams/dlms/aidon-example-2-encrypted.txt +++ /dev/null @@ -1,41 +0,0 @@ -7e a2 8b 03 05 00 9c 4f e6 e7 00 db 08 73 79 73 -74 69 74 6c 65 82 02 72 30 11 22 33 44 95 e9 9b -02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c d3 9d -78 85 b5 2b 94 3a 89 d8 73 67 bc 3e c9 7f 1f c9 -9f 51 0e 01 45 21 2f 11 d2 7e bd ae 98 93 1f 68 -7a 3b 82 e9 b2 ad 99 78 a4 0b d8 ba a1 99 44 e6 -7b a0 ee 48 72 44 37 3b 97 43 18 84 46 9d ce 89 -01 8b b2 7a a7 66 1d 39 49 32 61 c8 b7 30 1f 4f -d3 23 3c 21 e5 c8 7f 97 c5 43 0b 93 a2 19 5c 28 -70 f2 8a 1c d0 ec d3 ea 8a b1 c5 a3 26 f5 08 4b -e1 42 e6 a0 e7 0a b2 46 cd f3 f7 90 9e 9b ca 78 -e5 a5 74 5b 88 72 6b 93 4d c1 3f aa da f0 1b 38 -58 b6 c8 d9 f4 b9 79 48 0e f0 a5 8d 4f 6d 33 d5 -41 1f 2c 43 0a 79 44 23 f3 2e 47 d8 7c b5 d5 a6 -a7 f8 7e 8a 5a 2c 3e a7 a0 ec 04 16 25 0e 76 b1 -c7 09 51 bd 3f 59 46 37 0c 4e 0c cd 9e 97 ab 7f -9b bc 79 25 4b 99 c5 84 f2 14 cd 0d 95 d4 9e fe -e2 83 a2 9a 57 c5 28 66 bb 3b 97 da 00 07 c6 26 -4e 4f f6 92 28 01 46 b0 b2 f6 dd b2 22 cb 23 b6 -bc fa 50 16 9a e7 17 b7 90 78 ad 49 09 78 13 28 -d5 76 8b 9d 18 43 8d 20 1d bb b3 b0 74 58 39 8d -41 6b 82 d8 6f 34 4f 37 09 1f ea 3d eb 3e b8 c0 -38 48 3d 80 f1 32 e7 4c b6 37 b5 e5 24 2f f7 ec -1d 82 cc 0c e7 2e 46 4b 03 cf 6a 28 02 3f 42 9f -cb 63 93 32 24 56 bf bb 28 5e 1d 37 4e 93 63 66 -ea f4 67 db be ce 03 fb 7a b8 80 5c 0f 16 18 57 -f3 24 cc 10 26 2a 95 24 d3 2d 7f c3 70 d3 8a 06 -20 42 72 bb e6 b3 14 0d a4 43 62 0b 29 f9 f4 dc -32 bc fc b3 46 bb fd c5 13 8e c0 dc 94 17 7e 60 -b5 df 6d 1d 24 48 50 48 80 db f9 cf 9a ba cb b7 -bb 07 96 f3 66 9f f3 5d 46 39 57 04 3f d8 10 44 -c7 52 0f fa 56 65 26 4e 0d c3 22 7b 38 f3 35 6c -cb ba 4a 58 34 93 2e eb e9 7d ed 1a d2 55 38 f9 -f3 11 ae f9 1a 52 8f 59 76 74 e8 ee ee 4d 4d 2e -a2 ab c0 ee a1 73 ba 7e 0d bb 2a d7 6b 3c 31 b8 -9a f7 87 26 47 d6 41 c8 8d 27 09 2c e0 28 8a 15 -4d 16 50 a9 f3 71 52 db 6f ff 81 d8 38 e3 5b 41 -bf 58 41 6a e2 d4 0d cf 40 42 36 2b e8 13 c6 86 -7a f5 21 7d 09 c7 a5 be 68 7c f4 7c f7 d0 a0 41 -9b f0 72 f5 8c 8f e0 43 f2 8b 84 e6 bf 8d 09 ee -97 c8 31 de 81 eb dd c2 ca cb dc 76 7e diff --git a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json index e259d01..995c7a4 100644 --- a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/KMP5 ZABF001587315111\r\n0-0:96.1.1(205C4D246333034353537383234323121)\r\n1-0:1.8.1(00185.000*kWh)\r\n1-0:1.8.2(00084.000*kWh)\r\n1-0:2.8.1(00013.000*kWh)\r\n1-0:2.8.2(00019.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.98*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:17.0.0(999*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(3238313031453631373038389930337131)\r\n0-1:24.3.0(120517020000)(08)(60)(1)(0-1:24.2.1)(m3)\r\n(00124.477)\r\n0-1:24.4.0(1)\r\n!", "header": { "identifier": " ZABF001587315111", "xxx": "KMP", diff --git a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json index c6873f6..a653b1f 100644 --- a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1000\r\n\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(12345.678*kWh)\r\n1-0:1.8.2(12345.678*kWh)\r\n1-0:2.8.1(12345.678*kWh)\r\n1-0:2.8.2(12345.678*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(001.19*kW)\r\n1-0:2.7.0(000.00*kW)\r\n0-0:17.0.0(016*A)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1(303132333435363738)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.1.0(03)\r\n0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(00000.000)\r\n0-1:24.4.0(1)\r\n!\r\n", "header": { "identifier": "\\2MT382-1000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index 9b03327..0d6d739 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1 000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B", "header": { "identifier": "\\2MT382-1 000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json index 34edcb0..e5a3a95 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/XMX5LGBBFFB231216240\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(000101010000W)\r\n0-0:96.1.1(4530303034303031353931303932323134)\r\n1-0:1.8.1(001990.002*kWh)\r\n1-0:1.8.2(000000.000*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(999.9*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00023)\r\n0-0:96.7.9(00000)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!4F82", "header": { "identifier": "LGBBFFB231216240", "xxx": "XMX", diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index ddc13eb..814357d 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(40)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:17.0.0(016.1*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n0-1:24.1.0(03)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n0-1:24.4.0(1)\r\n!522B\r\n", "header": { "identifier": "\\2MT382-1000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index 6e81932..69cc8f6 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/KFM5KAIFA-METER\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(180306123056W)\r\n0-0:96.1.1(4530303033303030303032313234383133)\r\n1-0:1.8.1(004726.494*kWh)\r\n1-0:1.8.2(004844.281*kWh)\r\n1-0:2.8.1(003284.320*kWh)\r\n1-0:2.8.2(007764.691*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(01.869*kW)\r\n0-0:96.7.21(00013)\r\n0-0:96.7.9(00007)\r\n1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)\r\n1-0:32.32.0(00000)\r\n1-0:52.32.0(00000)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(003*A)\r\n1-0:71.7.0(002*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.688*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:42.7.0(00.778*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(00.403*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303136353631323033353830313133)\r\n0-1:24.2.1(180306120000W)(05359.919*m3)\r\n!A737", "header": { "identifier": "KAIFA-METER", "xxx": "KFM", diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index eb9158b..3e91478 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/XMX5LGBBFG10\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(170108161107W)\r\n0-0:96.1.1(4530303331303033303031363939353135)\r\n1-0:1.8.1(002074.842*kWh)\r\n1-0:1.8.2(000881.383*kWh)\r\n1-0:2.8.1(000010.981*kWh)\r\n1-0:2.8.2(000028.031*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(00.494*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)\r\n1-0:32.32.0(00000)\r\n1-0:32.36.0(00000)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n1-0:31.7.0(003*A)\r\n1-0:21.7.0(00.494*kW)\r\n1-0:22.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303139333430323231313938343135)\r\n0-1:24.2.1(170108160000W)(01234.000*m3)\r\n!D3B0", "header": { "identifier": "LGBBFG10", "xxx": "XMX", diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index a63a0f5..9749d67 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72:32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.1(3031203631203831)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:31.7.0.255(001*A)\r\n1-0:51.7.0.255(002*A)\r\n1-0:71.7.0.255(003*A)\r\n1-0:21.7.0.255(01.111*kW)\r\n1-0:41.7.0.255(02.222*kW)\r\n1-0:61.7.0.255(03.333*kW)\r\n1-0:22.7.0.255(04.444*kW)\r\n1-0:42.7.0.255(05.555*kW)\r\n1-0:62.7.0.255(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209110000W)(12785.123*m3)\r\n!CE7C", "header": { "identifier": "\\2MT382-1000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index 82e2ce9..993b4dc 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(210330192305S)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000009.533*kWh)\r\n1-0:1.8.2(000014.154*kWh)\r\n1-0:2.8.1(002248.911*kWh)\r\n1-0:2.8.2(005072.177*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.566*kW)\r\n0-0:96.7.21(00220)\r\n0-0:96.7.9(00034)\r\n1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)\r\n1-0:32.32.0(00024)\r\n1-0:52.32.0(00024)\r\n1-0:72.32.0(00024)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(227.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(004*A)\r\n1-0:51.7.0(004*A)\r\n1-0:71.7.0(004*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.068*kW)\r\n1-0:42.7.0(00.240*kW)\r\n1-0:62.7.0(00.257*kW)\r\n!D59D", "header": { "identifier": "\\T210-D ESMR5.0", "xxx": "Ene", diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index 999df63..17d8964 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/Ene5\\T210-D ESMR5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(180108202537W)\r\n0-0:96.1.1(serienummer)\r\n1-0:1.8.1(000000.855*kWh)\r\n1-0:1.8.2(000000.693*kWh)\r\n1-0:2.8.1(000000.084*kWh)\r\n1-0:2.8.2(000000.000*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.134*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00008)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(229.0*V)\r\n1-0:52.7.0(226.0*V)\r\n1-0:72.7.0(229.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.094*kW)\r\n1-0:41.7.0(00.040*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(serienummer)\r\n0-1:24.2.1(180108205500W)(00001.290*m3)\r\n!B055", "header": { "identifier": "\\T210-D ESMR5.0", "xxx": "Ene", diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index d46c005..5f6c5c2 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/Ene5\\T211 ESMR 5.0\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(240220170958W)\r\n0-0:96.1.1(4530303632303030303134353236323233)\r\n1-0:1.8.1(000565.971*kWh)\r\n1-0:1.8.2(000694.269*kWh)\r\n1-0:2.8.1(000006.754*kWh)\r\n1-0:2.8.2(000007.849*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.723*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00010)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(0)(0-0:96.7.19)\r\n1-0:32.32.0(00001)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n1-0:32.7.0(226.0*V)\r\n1-0:52.7.0(225.0*V)\r\n1-0:72.7.0(226.0*V)\r\n1-0:31.7.0(003*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.654*kW)\r\n1-0:41.7.0(00.069*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:42.1.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303732303033393634343938373139)\r\n0-1:24.2.1(240220171000W)(06362.120*m3)\r\n!B788", "header": { "identifier": "\\T211 ESMR 5.0", "xxx": "Ene", diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index e9fd376..1c466a7 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/EST5\\123456789_A\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(250319102812W)\r\n1-0:1.8.0(000201282*Wh)\r\n1-0:1.8.1(000134904*Wh)\r\n1-0:1.8.2(000066378*Wh)\r\n1-0:1.7.0(000000000*W)\r\n1-0:2.8.0(000320908*Wh)\r\n1-0:2.8.1(000317131*Wh)\r\n1-0:2.8.2(000003777*Wh)\r\n1-0:2.7.0(000003996*W)\r\n1-0:3.8.0(000036520*varh)\r\n1-0:3.8.1(000032377*varh)\r\n1-0:3.8.2(000004143*varh)\r\n1-0:3.7.0(000000000*var)\r\n1-0:4.8.0(000259742*varh)\r\n1-0:4.8.1(000169558*varh)\r\n1-0:4.8.2(000090184*varh)\r\n1-0:4.7.0(000000221*var)\r\n!FFFF\r\n", "header": { "identifier": "\\123456789_A", "xxx": "EST", diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index 6c21982..d17e420 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISK5\\2M550T-1011\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(181106140429W)\r\n0-0:96.1.1(4530303334303036383130353136343136)\r\n1-0:1.8.1(003808.351*kWh)\r\n1-0:1.8.2(002948.827*kWh)\r\n1-0:2.8.1(001285.951*kWh)\r\n1-0:2.8.2(002876.514*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.498*kW)\r\n0-0:96.7.21(00006)\r\n0-0:96.7.9(00003)\r\n1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)\r\n1-0:32.32.0(00003)\r\n1-0:52.32.0(00002)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00001)\r\n1-0:52.36.0(00001)\r\n1-0:72.36.0(00001)\r\n0-0:96.13.0()\r\n1-0:32.7.0(236.0*V)\r\n1-0:52.7.0(232.6*V)\r\n1-0:72.7.0(235.1*V)\r\n1-0:31.7.0(002*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.033*kW)\r\n1-0:61.7.0(00.132*kW)\r\n1-0:22.7.0(00.676*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(4730303339303031373030343630313137)\r\n0-1:24.2.1(181106140010W)(01569.646*m3)\r\n!1F28", "header": { "identifier": "\\2M550T-1011", "xxx": "ISK", diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index d418957..aa4e04c 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kwh)\r\n1-0:1.8.2(123456.789*kwh)\r\n1-0:2.8.1(123456.789*kwh)\r\n1-0:2.8.2(123456.789*kwh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kw)\r\n1-0:2.7.0(00.000*kw)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*v)\r\n1-0:52.7.0(220.2*v)\r\n1-0:72.7.0(220.3*v)\r\n1-0:31.7.0(001*a)\r\n1-0:51.7.0(002*a)\r\n1-0:71.7.0(003*a)\r\n1-0:21.7.0(01.111*kw)\r\n1-0:41.7.0(02.222*kw)\r\n1-0:61.7.0(03.333*kw)\r\n1-0:22.7.0(04.444*kw)\r\n1-0:42.7.0(05.555*kw)\r\n1-0:62.7.0(06.666*kw)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", "header": { "identifier": "\\2MT382-1000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index 8cd282d..aa4e04c 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1000\r\n\r\n1-3:0.2.8(50)\r\n0-0:1.0.0(101209113020W)\r\n0-0:96.1.1(4B384547303034303436333935353037)\r\n1-0:1.8.1(123456.789*kWh)\r\n1-0:1.8.2(123456.789*kWh)\r\n1-0:2.8.1(123456.789*kWh)\r\n1-0:2.8.2(123456.789*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(01.193*kW)\r\n1-0:2.7.0(00.000*kW)\r\n0-0:96.7.21(00004)\r\n0-0:96.7.9(00002)\r\n1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00000)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00003)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)\r\n1-0:32.7.0(220.1*V)\r\n1-0:52.7.0(220.2*V)\r\n1-0:72.7.0(220.3*V)\r\n1-0:31.7.0(001*A)\r\n1-0:51.7.0(002*A)\r\n1-0:71.7.0(003*A)\r\n1-0:21.7.0(01.111*kW)\r\n1-0:41.7.0(02.222*kW)\r\n1-0:61.7.0(03.333*kW)\r\n1-0:22.7.0(04.444*kW)\r\n1-0:42.7.0(05.555*kW)\r\n1-0:62.7.0(06.666*kW)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(3232323241424344313233343536373839)\r\n0-1:24.2.1(101209112500W)(12785.123*m3)\r\n!EF2F\r\n", "header": { "identifier": "\\2MT382-1000", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index 643deca..f1abde6 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/Lux5\\253694471_M\r\n1-3:0.2.8(42)\r\n0-0:1.0.0(200706104157S)\r\n0-0:42.0.0(53414731303330373930303032353734)\r\n1-0:1.8.0(000025.653*kWh)\r\n1-0:2.8.0(000000.040*kWh)\r\n1-0:3.8.0(000000.835*kvarh)\r\n1-0:4.8.0(000063.781*kvarh)\r\n1-0:1.7.0(00.005*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:3.7.0(00.000*kvar)\r\n1-0:4.7.0(00.000*kvar)\r\n0-0:17.0.0(069.0*kVA)\r\n1-0:9.7.0(00.021*kVA)\r\n1-0:10.7.0(00.000*kVA)\r\n1-1:31.4.0(100*A)(-063*A)\r\n0-0:96.3.10(1)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-0:96.7.21(00099)\r\n1-0:32.32.0(00040)\r\n1-0:52.32.0(00003)\r\n1-0:72.32.0(00002)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n0-0:96.13.0()\r\n0-0:96.13.2()\r\n0-0:96.13.3()\r\n0-0:96.13.4()\r\n0-0:96.13.5()\r\n1-0:32.7.0(233.0*V)\r\n1-0:52.7.0(000.0*V)\r\n1-0:72.7.0(001.0*V)\r\n1-0:31.7.0(000*A)\r\n1-0:51.7.0(000*A)\r\n1-0:71.7.0(000*A)\r\n1-0:21.7.0(00.005*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:23.7.0(00.000*kvar)\r\n1-0:43.7.0(00.000*kvar)\r\n1-0:63.7.0(00.000*kvar)\r\n1-0:24.7.0(00.000*kvar)\r\n1-0:44.7.0(00.000*kvar)\r\n1-0:64.7.0(00.000*kvar)\r\n0-1:24.1.0(003)\r\n0-1:96.1.0(464C4F313839393030303630333535)\r\n0-1:24.2.1(200706103140S)(00000.006*m3)\r\n0-1:24.4.0(0)\r\n0-2:24.1.0(007)\r\n0-2:96.1.0()\r\n0-2:24.2.1(632525252525S)(00000.000)\r\n0-2:24.4.0(1)\r\n0-3:24.1.0(007)\r\n0-3:96.1.0()\r\n0-3:24.2.1(632525252525S)(00000.000)\r\n0-3:24.4.0(1)\r\n0-4:24.1.0(003)\r\n0-4:96.1.0(454C53333533353839393830333030)\r\n0-4:24.2.1(200706102900S)(00028.103*m3)\r\n0-4:24.4.0(1)\r\n!8B52", "header": { "identifier": "\\253694471_M", "xxx": "Lux", diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json index 1dc5961..f7c1b86 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/FLU5\\253769484_A\r\n0-0:96.1.4(50221)\r\n1-0:94.32.1(400)\r\n0-0:96.1.1(3153414733313031303231363035)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512135409S)\r\n1-0:1.8.1(000000.034*kWh)\r\n1-0:1.8.2(000015.758*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.011*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:41.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:62.7.0(00.000*kW)\r\n1-0:32.7.0(234.7*V)\r\n1-0:52.7.0(234.7*V)\r\n1-0:72.7.0(234.7*V)\r\n1-0:31.7.0(000.00*A)\r\n1-0:51.7.0(000.00*A)\r\n1-0:71.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", "header": { "identifier": "\\253769484_A", "xxx": "FLU", diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json index 0e1899d..a48e8a7 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/FLU5\\253770234_A\r\n0-0:96.1.4(50221)\r\n0-0:96.1.1(3153414731313030303030323331)\r\n0-0:96.1.2(353431343430303132333435363738393030)\r\n0-0:1.0.0(200512145552S)\r\n1-0:1.8.1(000000.915*kWh)\r\n1-0:1.8.2(000001.955*kWh)\r\n1-0:2.8.1(000000.000*kWh)\r\n1-0:2.8.2(000000.030*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.4.0(02.351*kW)\r\n1-0:1.6.0(200509134558S)(02.589*kW)\r\n0-0:98.1.0(3)(1-0:1.6.0)(1-0:1.6.0)(200501000000S)(200423192538S)(03.695*kW)(200401000000S)(200305122139S)(05.980*kW)(200301000000S)(200210035421W)(04.318*kW)\r\n1-0:1.7.0(00.000*kW)\r\n1-0:2.7.0(00.000*kW)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(00.000*kW)\r\n1-0:32.7.0(234.6*V)\r\n1-0:31.7.0(000.00*A)\r\n0-0:96.3.10(1)\r\n0-0:17.0.0(99.999*kW)\r\n1-0:31.4.0(999.99*A)\r\n0-1:96.3.10(0)\r\n0-2:96.3.10(0)\r\n0-3:96.3.10(0)\r\n0-4:96.3.10(0)\r\n0-0:96.13.0()\r\n0-1:24.1.0(003)\r\n0-1:96.1.1(37464C4F32313139303333373333)\r\n0-1:96.1.2(353431343430303132333435363738393030)\r\n0-1:24.4.0(1)\r\n0-1:24.2.3(200512134558S)(00112.384*m3)\r\n0-2:24.1.0(007)\r\n0-2:96.1.1(3853414731323334353637383930)\r\n0-2:96.1.2(353431343430303132333435363738393033)\r\n0-2:24.2.3(200512134558S)(00872.234*m3)\r\n!1234", "header": { "identifier": "\\253770234_A", "xxx": "FLU", diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json index f365e47..c10f351 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0(test-/-test)\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", "header": { "identifier": "\\2MT382-1003", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json index cf60e34..62bd4a8 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/ISk5\\2MT382-1003\r\n\r\n0-0:96.1.1(00112233445566778899aabbccddeeff)\r\n1-0:1.8.1(39837.604*kWh)\r\n1-0:1.8.2(30477.225*kWh)\r\n1-0:2.8.1(05174.479*kWh)\r\n1-0:2.8.2(11772.946*kWh)\r\n0-0:96.14.0(0002)\r\n1-0:1.7.0(0000.00*kW)\r\n1-0:2.7.0(0000.14*kW)\r\n0-0:17.0.0(0999.00*kW)\r\n0-0:96.3.10(1)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n0-1:24.1.0(3)\r\n0-1:96.1.0(0011223344556677889900112233445566)\r\n0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)\r\n(13032.850)\r\n0-1:24.4.0(1)\r\n", "header": { "identifier": "\\2MT382-1003", "xxx": "ISk", diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index 17209cf..9d3d617 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/KAM5\r\n\r\n0-0:1.0.0(250000000000W)\r\n1-0:1.8.0(00000123.321*kWh)\r\n1-0:2.8.0(00000456.654*kWh)\r\n1-0:3.8.0(00001234.432*kVArh)\r\n1-0:4.8.0(00005678.765*kVArh)\r\n1-0:1.7.0(0002.424*kW)\r\n1-0:2.7.0(0000.000*kW)\r\n1-0:3.7.0(0001.229*kVAr)\r\n1-0:4.7.0(0000.000*kVAr)\r\n1-0:21.7.0(0000.682*kW)\r\n1-0:41.7.0(0000.750*kW)\r\n1-0:61.7.0(0000.992*kW)\r\n1-0:22.7.0(0000.000*kW)\r\n1-0:42.7.0(0000.000*kW)\r\n1-0:62.7.0(0000.000*kW)\r\n1-0:23.7.0(0000.391*kVAr)\r\n1-0:43.7.0(0000.490*kVAr)\r\n1-0:63.7.0(0000.348*kVAr)\r\n1-0:24.7.0(0000.000*kVAr)\r\n1-0:44.7.0(0000.000*kVAr)\r\n1-0:64.7.0(0000.000*kVAr)\r\n1-0:32.7.0(225.4*V)\r\n1-0:52.7.0(229.4*V)\r\n1-0:72.7.0(225.9*V)\r\n1-0:31.7.0(003.4*A)\r\n1-0:51.7.0(003.9*A)\r\n1-0:71.7.0(004.6*A)\r\n!3AF8\r\n", "header": { "identifier": "", "xxx": "KAM", diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index c19fa92..7b319b6 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/GRE5\\\\12341234_A\r\n\r\n0-0:1.0.0(123412341234W)\r\n0-0:0.0.0(123412341234)\r\n1-1:1.8.1(001595.070*kWh)\r\n1-1:1.8.2(005216.778*kWh)\r\n1-1:2.8.1(004478.038*kWh)\r\n1-1:2.8.2(000004.648*kWh)\r\n0-0:96.14.0(0001)\r\n1-1:1.7.0(01.622*kW)\r\n1-1:2.7.0(00.000*kW)\r\n0-0:96.7.21(00018)\r\n0-0:96.7.9(00004)\r\n1-0:99.97.0(2)(0-0:96.7.19)(240523144934S)(0005564711*s)(241109083138W)(0000000124*s)\r\n1-0:32.32.0(00002)\r\n1-0:52.32.0(00001)\r\n1-0:72.32.0(00001)\r\n1-0:32.36.0(00000)\r\n1-0:52.36.0(00000)\r\n1-0:72.36.0(00000)\r\n1-0:32.7.0(238.9*V)\r\n1-0:52.7.0(231.9*V)\r\n1-0:72.7.0(239.7*V)\r\n1-0:31.7.0(005.15*A)\r\n1-0:51.7.0(017.19*A)\r\n1-0:71.7.0(005.02*A)\r\n0-0:96.13.0()\r\n0-0:96.1.4(12345)\r\n0-0:96.1.2(202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020)\r\n1-0:21.7.0(00.000*kW)\r\n1-0:22.7.0(01.175*kW)\r\n1-0:41.7.0(03.946*kW)\r\n1-0:42.7.0(00.000*kW)\r\n1-0:61.7.0(00.000*kW)\r\n1-0:62.7.0(01.148*kW)\r\n0-0:96.13.1()\r\n1-1:1.6.0(250315014500W)(15.206*kW)\r\n0-0:98.1.0(12)\r\n1-0:1.4.0(00.288*kW)\r\n!3BA5\r\n", "header": { "identifier": "\\\\12341234_A", "xxx": "GRE", diff --git a/tests/telegrams/dsmr/unknown-xmx-1.json b/tests/telegrams/dsmr/unknown-xmx-1.json index 1b2060f..2448f42 100644 --- a/tests/telegrams/dsmr/unknown-xmx-1.json +++ b/tests/telegrams/dsmr/unknown-xmx-1.json @@ -1,6 +1,5 @@ { "dsmr": { - "raw": "/XMX5XMXABCE000012345\r\n\r\n0-0:96.1.1(0123456789ABCDEF)\r\n1-0:1.8.1(11667.440*kWh)\r\n1-0:1.8.2(11781.558*kWh)\r\n1-0:2.8.1(00000.000*kWh)\r\n1-0:2.8.2(00000.000*kWh)\r\n0-0:96.14.0(0001)\r\n1-0:1.7.0(0000.55*kW)\r\n1-0:2.7.0(0000.00*kW)\r\n0-0:96.13.1()\r\n0-0:96.13.0()\r\n!", "header": { "identifier": "XMXABCE000012345", "xxx": "XMX", diff --git a/tests/test-utils.ts b/tests/test-utils.ts index b8689c1..47c2087 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -6,10 +6,7 @@ import { ENCRYPTED_DLMS_SYSTEM_TITLE_LEN, ENCRYPTED_DLMS_TELEGRAM_SOF, } from '../src/protocols/encryption.js'; -import { - HDLC_TELEGRAM_SOF_EOF, - HDLC_FORMAT_START, -} from '../src/protocols/hdlc.js'; +import { HDLC_TELEGRAM_SOF_EOF, HDLC_FORMAT_START } from '../src/protocols/hdlc.js'; import { calculateCrc16IbmSdlc } from '../src/util/crc.js'; export const TEST_DECRYPTION_KEY = Buffer.from('0123456789abcdef01234567890abcdef', 'hex'); diff --git a/tools/parse-dlms.ts b/tools/parse-dlms.ts index 93cbb54..4d04104 100644 --- a/tools/parse-dlms.ts +++ b/tools/parse-dlms.ts @@ -79,17 +79,25 @@ console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); const result: HdlcParserResult = { hdlc: { - raw: '', - header: { - destinationAddress: 0, - sourceAddress: 0, - crc: { - value: 0, - valid: false, + headers: [ + { + destinationAddress: 0, + sourceAddress: 0, + crc: { + value: 0, + valid: false, + }, }, - }, + ], + footers: [ + { + crc: { + value: 0, + valid: false, + }, + }, + ], crc: { - value: 0, valid: false, }, }, diff --git a/tools/parse-hdlc.ts b/tools/parse-hdlc.ts index e711e89..6acfe79 100644 --- a/tools/parse-hdlc.ts +++ b/tools/parse-hdlc.ts @@ -100,17 +100,25 @@ console.log(dlmsDataTypeToList(dlmsContent.data, ' ')); const result: HdlcParserResult = { hdlc: { - raw: '', - header: { - destinationAddress: 0, - sourceAddress: 0, - crc: { - value: 0, - valid: false, + headers: [ + { + destinationAddress: 0, + sourceAddress: 0, + crc: { + value: 0, + valid: false, + }, }, - }, + ], + footers: [ + { + crc: { + value: 0, + valid: false, + }, + }, + ], crc: { - value: 0, valid: false, }, }, diff --git a/tools/update-test-telegrams.ts b/tools/update-test-telegrams.ts index 8b911f8..111e6db 100644 --- a/tools/update-test-telegrams.ts +++ b/tools/update-test-telegrams.ts @@ -80,19 +80,13 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr stream: passthrough, callback: (error, result) => { if (error) { - if (error instanceof Error) { - results.push({ - error: { - message: error.message, - name: error.name, - stack: error.stack, - }, - }); - } else { - results.push({ - error, - }); - } + results.push({ + error: { + message: error.message, + name: error.name, + stack: error.stack, + }, + }); } else if (result) { results.push(result); } @@ -141,11 +135,7 @@ import { decodeHdlcHeader, decodeLlcHeader, HDLC_FOOTER_LENGTH } from '../src/pr key: TEST_DECRYPTION_KEY, }); - const llcBuffer = Buffer.from([ - llc.destination, - llc.source, - llc.quality - ]); + const llcBuffer = Buffer.from([llc.destination, llc.source, llc.quality]); const frameWithAad = wrapHdlcFrame(Buffer.concat([llcBuffer, encryptedAad])); const frameWithoutAad = wrapHdlcFrame(Buffer.concat([llcBuffer, encryptedWithoutAad])); From 77156724a987f726b0f878aae39ab4bccc962a7a Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 27 May 2025 10:47:01 +0200 Subject: [PATCH 07/18] chore(cosem): fix converting kw to w --- src/protocols/cosem.ts | 10 +++++----- tests/telegrams/dlms/aidon-example-1.json | 2 +- tests/telegrams/dlms/aidon-example-2-segmented.json | 2 +- tests/telegrams/dlms/aidon-example-2.json | 2 +- tests/telegrams/dsmr/dsmr-2.2-kfm-1.json | 2 +- tests/telegrams/dsmr/dsmr-3.0-spec-example.json | 2 +- tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 2 +- tests/telegrams/dsmr/dsmr-4.0-spec-example.json | 2 +- tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 2 +- tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 2 +- tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-est-units.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 2 +- .../dsmr/dsmr-5.0-spec-example-lowercase.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-spec-example.json | 2 +- .../telegrams/dsmr/dsmr-luxembourgh-spec-example.json | 2 +- .../dsmr/iskra-mt-382-no-crc-with-text-message.json | 2 +- tests/telegrams/dsmr/iskra-mt-382-no-crc.json | 2 +- .../dsmr/kamstrup-OMNIA-e-meter-three-phase.json | 2 +- tests/telegrams/dsmr/sagemcom-xt211.json | 2 +- tests/telegrams/dsmr/unknown-xmx-1.json | 2 +- 24 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/protocols/cosem.ts b/src/protocols/cosem.ts index 4a1642b..6139380 100644 --- a/src/protocols/cosem.ts +++ b/src/protocols/cosem.ts @@ -164,14 +164,14 @@ export const CosemLibrary = new CosemLibraryInternal() result.electricity.currentTariff = valueNumber; }) .addNumberParser('1-*:1.7.0', ({ valueNumber, unit, result }) => { - if (unit?.toLowerCase() === 'w') { + if (unit?.toLowerCase() === 'kw') { valueNumber *= 1000; } result.electricity.powerReceivedTotal = valueNumber; }) .addNumberParser('1-*:2.7.0', ({ valueNumber, unit, result }) => { - if (unit?.toLowerCase() === 'w') { + if (unit?.toLowerCase() === 'kw') { valueNumber *= 1000; } @@ -237,7 +237,7 @@ export const CosemLibrary = new CosemLibraryInternal() // The currents for DLMS (in BasicList/DescribedList) mode are in 10 mA to // give more precision without using floats. if (dlms?.useDefaultScalar) { - valueNumber = valueNumber / 100; + valueNumber /= 100; } result.electricity.current = result.electricity.current ?? {}; @@ -245,7 +245,7 @@ export const CosemLibrary = new CosemLibraryInternal() }) .addNumberParser('1-*:51.7.0', ({ valueNumber, result, dlms }) => { if (dlms?.useDefaultScalar) { - valueNumber = valueNumber / 100; + valueNumber /= 100; } result.electricity.current = result.electricity.current ?? {}; @@ -253,7 +253,7 @@ export const CosemLibrary = new CosemLibraryInternal() }) .addNumberParser('1-*:71.7.0', ({ valueNumber, result, dlms }) => { if (dlms?.useDefaultScalar) { - valueNumber = valueNumber / 100; + valueNumber /= 100; } result.electricity.current = result.electricity.current ?? {}; diff --git a/tests/telegrams/dlms/aidon-example-1.json b/tests/telegrams/dlms/aidon-example-1.json index 4111a34..a2c10c5 100644 --- a/tests/telegrams/dlms/aidon-example-1.json +++ b/tests/telegrams/dlms/aidon-example-1.json @@ -44,7 +44,7 @@ ] }, "electricity": { - "powerReceivedTotal": 1362000, + "powerReceivedTotal": 1362, "powerReturnedTotal": 0, "current": { "l1": 9.3 diff --git a/tests/telegrams/dlms/aidon-example-2-segmented.json b/tests/telegrams/dlms/aidon-example-2-segmented.json index 99f5a3c..d01075d 100644 --- a/tests/telegrams/dlms/aidon-example-2-segmented.json +++ b/tests/telegrams/dlms/aidon-example-2-segmented.json @@ -90,7 +90,7 @@ ] }, "electricity": { - "powerReceivedTotal": 1122000, + "powerReceivedTotal": 1122, "powerReturnedTotal": 0, "current": { "l1": 0, diff --git a/tests/telegrams/dlms/aidon-example-2.json b/tests/telegrams/dlms/aidon-example-2.json index d015740..ac4724f 100644 --- a/tests/telegrams/dlms/aidon-example-2.json +++ b/tests/telegrams/dlms/aidon-example-2.json @@ -62,7 +62,7 @@ ] }, "electricity": { - "powerReceivedTotal": 1122000, + "powerReceivedTotal": 1122, "powerReturnedTotal": 0, "current": { "l1": 0, diff --git a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json index 995c7a4..80bd529 100644 --- a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json @@ -33,7 +33,7 @@ } }, "currentTariff": 1, - "powerReceivedTotal": 0.98, + "powerReceivedTotal": 980, "powerReturnedTotal": 0 }, "mBus": { diff --git a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json index a653b1f..512cfb8 100644 --- a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json @@ -33,7 +33,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.19, + "powerReceivedTotal": 1190, "powerReturnedTotal": 0 }, "mBus": { diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index 0d6d739..acd6bfa 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -53,7 +53,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.193, + "powerReceivedTotal": 1193, "powerReturnedTotal": 0 }, "mBus": { diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index 814357d..6ab7a8d 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -53,7 +53,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.193, + "powerReceivedTotal": 1193, "powerReturnedTotal": 0 }, "mBus": { diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index 69cc8f6..866889b 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -51,7 +51,7 @@ }, "currentTariff": 2, "powerReceivedTotal": 0, - "powerReturnedTotal": 1.869, + "powerReturnedTotal": 1869, "current": { "l1": 3, "l2": 3, diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index 3e91478..01567e8 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -46,7 +46,7 @@ } }, "currentTariff": 1, - "powerReceivedTotal": 0.494, + "powerReceivedTotal": 494, "powerReturnedTotal": 0, "current": { "l1": 3 diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index 9749d67..6eb99c3 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -59,7 +59,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.193, + "powerReceivedTotal": 1193, "powerReturnedTotal": 0 }, "mBus": { diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index 993b4dc..bd69e65 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -50,7 +50,7 @@ }, "currentTariff": 2, "powerReceivedTotal": 0, - "powerReturnedTotal": 0.566, + "powerReturnedTotal": 566, "voltage": { "l1": 226, "l2": 227, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index 17d8964..fd69396 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -49,7 +49,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 0.134, + "powerReceivedTotal": 134, "powerReturnedTotal": 0, "voltage": { "l1": 229, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index 5f6c5c2..ed88d52 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -50,7 +50,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 0.723, + "powerReceivedTotal": 723, "powerReturnedTotal": 0, "voltage": { "l1": 226, diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index 1c466a7..3c904fb 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -44,7 +44,7 @@ } }, "powerReceivedTotal": 0, - "powerReturnedTotal": 3996000 + "powerReturnedTotal": 3996 }, "mBus": {} } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index d17e420..edd117a 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -50,7 +50,7 @@ }, "currentTariff": 2, "powerReceivedTotal": 0, - "powerReturnedTotal": 0.498, + "powerReturnedTotal": 498, "voltage": { "l1": 236, "l2": 232.6, diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index aa4e04c..e11724d 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -49,7 +49,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.193, + "powerReceivedTotal": 1193, "powerReturnedTotal": 0, "voltage": { "l1": 220.1, diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index aa4e04c..e11724d 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -49,7 +49,7 @@ } }, "currentTariff": 2, - "powerReceivedTotal": 1.193, + "powerReceivedTotal": 1193, "powerReturnedTotal": 0, "voltage": { "l1": 220.1, diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index f1abde6..3e531bf 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -65,7 +65,7 @@ "received": 25653, "returned": 40 }, - "powerReceivedTotal": 0.005, + "powerReceivedTotal": 5, "powerReturnedTotal": 0, "voltage": { "l1": 233, diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json index c10f351..c21e4f5 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json @@ -34,7 +34,7 @@ }, "currentTariff": 2, "powerReceivedTotal": 0, - "powerReturnedTotal": 0.14 + "powerReturnedTotal": 140 }, "mBus": { "1": { diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json index 62bd4a8..420ddca 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json @@ -34,7 +34,7 @@ }, "currentTariff": 2, "powerReceivedTotal": 0, - "powerReturnedTotal": 0.14 + "powerReturnedTotal": 140 }, "mBus": { "1": { diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index 9d3d617..77c5888 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -34,7 +34,7 @@ "received": 123321, "returned": 456654 }, - "powerReceivedTotal": 2.424, + "powerReceivedTotal": 2424, "powerReturnedTotal": 0, "powerReceived": { "l1": 0.682, diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index 7b319b6..104af03 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -54,7 +54,7 @@ } }, "currentTariff": 1, - "powerReceivedTotal": 1.622, + "powerReceivedTotal": 1622, "powerReturnedTotal": 0, "voltage": { "l1": 238.9, diff --git a/tests/telegrams/dsmr/unknown-xmx-1.json b/tests/telegrams/dsmr/unknown-xmx-1.json index 2448f42..4b6cc44 100644 --- a/tests/telegrams/dsmr/unknown-xmx-1.json +++ b/tests/telegrams/dsmr/unknown-xmx-1.json @@ -27,7 +27,7 @@ } }, "currentTariff": 1, - "powerReceivedTotal": 0.55, + "powerReceivedTotal": 550, "powerReturnedTotal": 0 }, "mBus": {} From 2b4f40ac937ee777196beffa3e792e34be2bf273 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 27 May 2025 10:54:08 +0200 Subject: [PATCH 08/18] feat(dsmr): set known/unknown cosem objects in result --- src/protocols/dsmr.ts | 13 +++-- tests/telegrams/dsmr/dsmr-2.2-kfm-1.json | 25 +++++++-- .../telegrams/dsmr/dsmr-3.0-spec-example.json | 25 +++++++-- tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 36 ++++++++++-- tests/telegrams/dsmr/dsmr-4.0-isk-2.json | 30 +++++++--- .../telegrams/dsmr/dsmr-4.0-spec-example.json | 36 ++++++++++-- tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 42 ++++++++++++-- tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 32 +++++++++-- .../dsmr/dsmr-4.2.2-spec-example.json | 42 +++++++++++--- tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 41 ++++++++++++-- tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 44 +++++++++++++-- tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 45 +++++++++++++-- tests/telegrams/dsmr/dsmr-5.0-est-units.json | 28 +++++++--- tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 44 +++++++++++++-- .../dsmr/dsmr-5.0-spec-example-lowercase.json | 44 +++++++++++++-- .../telegrams/dsmr/dsmr-5.0-spec-example.json | 44 +++++++++++++-- .../dsmr/dsmr-luxembourgh-spec-example.json | 56 ++++++++++++++++--- .../dsmr/emucs-p1-v2.1.1-spec-example-1.json | 44 ++++++++++++--- .../dsmr/emucs-p1-v2.1.1-spec-example-2.json | 36 +++++++++--- ...iskra-mt-382-no-crc-with-text-message.json | 25 +++++++-- tests/telegrams/dsmr/iskra-mt-382-no-crc.json | 25 +++++++-- .../kamstrup-OMNIA-e-meter-three-phase.json | 35 +++++++++--- tests/telegrams/dsmr/sagemcom-xt211.json | 48 +++++++++++++--- tests/telegrams/dsmr/unknown-xmx-1.json | 13 ++++- 24 files changed, 705 insertions(+), 148 deletions(-) diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts index 4db7673..05075b5 100644 --- a/src/protocols/dsmr.ts +++ b/src/protocols/dsmr.ts @@ -85,12 +85,15 @@ const decodeDsmrCosemLine = ({ const { obisCode, consumedChars } = parseObisCodeFromString(line); if (obisCode === null) { + result.dsmr.unknownLines = result.dsmr.unknownLines ?? []; + result.dsmr.unknownLines.push(line); return false; } const parser = CosemLibrary.getParser(obisCode); if (!parser) { + result.cosem.unknownObjects.push(line); return false; } @@ -101,8 +104,10 @@ const decodeDsmrCosemLine = ({ const regexResult = StringTypeRegex.exec(lineWithoutObisCode); if (!regexResult) { + result.cosem.unknownObjects.push(line); return false; } + result.cosem.knownObjects.push(line); const valueString = regexResult[1] ?? ''; @@ -122,8 +127,10 @@ const decodeDsmrCosemLine = ({ const regexResult = NumberTypeRegex.exec(lineWithoutObisCode); if (!regexResult) { + result.cosem.unknownObjects.push(line); return false; } + result.cosem.knownObjects.push(line); const valueString = regexResult[1] ?? ''; const unit = regexResult[2] ? regexResult[2].slice(1) : null; @@ -149,6 +156,7 @@ const decodeDsmrCosemLine = ({ return true; } case 'raw': { + result.cosem.knownObjects.push(line); parser.callback({ result, obisCode, @@ -239,10 +247,7 @@ export const parseDsmr = (options: DsmrParserOptions): DsmrParserResult => { lineNumber, }); - if (!isLineParsed) { - result.dsmr.unknownLines = result.dsmr.unknownLines ?? []; - result.dsmr.unknownLines.push(line); - } else { + if (isLineParsed) { objectsParsed++; } } diff --git a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json index 80bd529..1156614 100644 --- a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json @@ -6,16 +6,31 @@ "z": "5" }, "unknownLines": [ + "(00124.477)" + ] + }, + "cosem": { + "unknownObjects": [ "0-0:17.0.0(999*A)", "0-0:96.3.10(1)", - "(00124.477)", "0-1:24.4.0(1)" + ], + "knownObjects": [ + "0-0:96.1.1(205C4D246333034353537383234323121)", + "1-0:1.8.1(00185.000*kWh)", + "1-0:1.8.2(00084.000*kWh)", + "1-0:2.8.1(00013.000*kWh)", + "1-0:2.8.2(00019.000*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(0000.98*kW)", + "1-0:2.7.0(0000.00*kW)", + "0-0:96.13.1()", + "0-0:96.13.0()", + "0-1:24.1.0(3)", + "0-1:96.1.0(3238313031453631373038389930337131)", + "0-1:24.3.0(120517020000)(08)(60)(1)(0-1:24.2.1)(m3)" ] }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] - }, "metadata": { "equipmentId": "205C4D246333034353537383234323121", "numericMessage": 0, diff --git a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json index 512cfb8..e7a0606 100644 --- a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json @@ -6,16 +6,31 @@ "z": "5" }, "unknownLines": [ + "(00000.000)" + ] + }, + "cosem": { + "unknownObjects": [ "0-0:17.0.0(016*A)", "0-0:96.3.10(1)", - "(00000.000)", "0-1:24.4.0(1)" + ], + "knownObjects": [ + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(12345.678*kWh)", + "1-0:1.8.2(12345.678*kWh)", + "1-0:2.8.1(12345.678*kWh)", + "1-0:2.8.2(12345.678*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(001.19*kW)", + "1-0:2.7.0(000.00*kW)", + "0-0:96.13.1(303132333435363738)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.1.0(03)", + "0-1:24.3.0(090212160000)(00)(60)(1)(0-1:24.2.1)(m3)" ] }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] - }, "metadata": { "equipmentId": "4B384547303034303436333935353037", "numericMessage": 303132333435363700, diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index acd6bfa..f8fa86d 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -6,11 +6,8 @@ "z": "5" }, "unknownLines": [ - "0-0:17.0.0(016.1*kW)", - "0-0:96.3.10(1)", "1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)", - "1-0:72:32.0(00000)", - "0-1:24.4.0(1)" + "1-0:72:32.0(00000)" ], "crc": { "value": 21035, @@ -18,8 +15,35 @@ } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "0-0:17.0.0(016.1*kW)", + "0-0:96.3.10(1)", + "0-1:24.4.0(1)" + ], + "knownObjects": [ + "1-3:0.2.8(40)", + "0-0:1.0.0(101209113020W)", + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(123456.789*kWh)", + "1-0:1.8.2(123456.789*kWh)", + "1-0:2.8.1(123456.789*kWh)", + "1-0:2.8.2(123456.789*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(01.193*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00002)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00003)", + "1-0:72.36.0(00000)", + "0-0:96.13.1(3031203631203831)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "0-1:24.1.0(03)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.2.1(101209110000W)(12785.123*m3)" + ] }, "metadata": { "dsmrVersion": 4, diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json index e5a3a95..c2d9b4e 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json @@ -5,19 +5,35 @@ "xxx": "XMX", "z": "5" }, - "unknownLines": [ - "0-0:17.0.0(999.9*kW)", - "0-0:96.3.10(1)", - "1-0:99.97.0(0)(0-0:96.7.19)" - ], "crc": { "value": 20354, "valid": true } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "0-0:17.0.0(999.9*kW)", + "0-0:96.3.10(1)", + "1-0:99.97.0(0)(0-0:96.7.19)" + ], + "knownObjects": [ + "1-3:0.2.8(40)", + "0-0:1.0.0(000101010000W)", + "0-0:96.1.1(4530303034303031353931303932323134)", + "1-0:1.8.1(001990.002*kWh)", + "1-0:1.8.2(000000.000*kWh)", + "1-0:2.8.1(000000.000*kWh)", + "1-0:2.8.2(000000.000*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00023)", + "0-0:96.7.9(00000)", + "1-0:32.32.0(00000)", + "1-0:32.36.0(00000)", + "0-0:96.13.1()", + "0-0:96.13.0()" + ] }, "metadata": { "dsmrVersion": 4, diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index 6ab7a8d..000c46d 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -6,11 +6,8 @@ "z": "5" }, "unknownLines": [ - "0-0:17.0.0(016.1*kW)", - "0-0:96.3.10(1)", "1-0:99:97.0(2)(0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(00000000301*s)", - "1-0:72:32.0(00000)", - "0-1:24.4.0(1)" + "1-0:72:32.0(00000)" ], "crc": { "value": 21035, @@ -18,8 +15,35 @@ } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "0-0:17.0.0(016.1*kW)", + "0-0:96.3.10(1)", + "0-1:24.4.0(1)" + ], + "knownObjects": [ + "1-3:0.2.8(40)", + "0-0:1.0.0(101209113020W)", + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(123456.789*kWh)", + "1-0:1.8.2(123456.789*kWh)", + "1-0:2.8.1(123456.789*kWh)", + "1-0:2.8.2(123456.789*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(01.193*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00002)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00003)", + "1-0:72.36.0(00000)", + "0-0:96.13.1(3031203631203831)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "0-1:24.1.0(03)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.2.1(101209110000W)(12785.123*m3)" + ] }, "metadata": { "dsmrVersion": 4, diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index 866889b..24e34cd 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -5,17 +5,49 @@ "xxx": "KFM", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)" - ], "crc": { "value": 42807, "valid": true } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(1)(0-0:96.7.19)(000101000024W)(2147483647*s)" + ], + "knownObjects": [ + "1-3:0.2.8(42)", + "0-0:1.0.0(180306123056W)", + "0-0:96.1.1(4530303033303030303032313234383133)", + "1-0:1.8.1(004726.494*kWh)", + "1-0:1.8.2(004844.281*kWh)", + "1-0:2.8.1(003284.320*kWh)", + "1-0:2.8.2(007764.691*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(01.869*kW)", + "0-0:96.7.21(00013)", + "0-0:96.7.9(00007)", + "1-0:32.32.0(00000)", + "1-0:52.32.0(00000)", + "1-0:72.32.0(00000)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "0-0:96.13.1()", + "0-0:96.13.0()", + "1-0:31.7.0(003*A)", + "1-0:51.7.0(003*A)", + "1-0:71.7.0(002*A)", + "1-0:21.7.0(00.000*kW)", + "1-0:22.7.0(00.688*kW)", + "1-0:41.7.0(00.000*kW)", + "1-0:42.7.0(00.778*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:62.7.0(00.403*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(4730303136353631323033353830313133)", + "0-1:24.2.1(180306120000W)(05359.919*m3)" + ] }, "metadata": { "dsmrVersion": 4.2, diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index 01567e8..b0ffffe 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -5,17 +5,39 @@ "xxx": "XMX", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)" - ], "crc": { "value": 54192, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(3)(0-0:96.7.19)(160315184219W)(0000000310*s)(160207164837W)(0000000981*s)(151118085623W)(0000502496*s)" + ], + "knownObjects": [ + "1-3:0.2.8(42)", + "0-0:1.0.0(170108161107W)", + "0-0:96.1.1(4530303331303033303031363939353135)", + "1-0:1.8.1(002074.842*kWh)", + "1-0:1.8.2(000881.383*kWh)", + "1-0:2.8.1(000010.981*kWh)", + "1-0:2.8.2(000028.031*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(00.494*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00003)", + "1-0:32.32.0(00000)", + "1-0:32.36.0(00000)", + "0-0:96.13.1()", + "0-0:96.13.0()", + "1-0:31.7.0(003*A)", + "1-0:21.7.0(00.494*kW)", + "1-0:22.7.0(00.000*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(4730303139333430323231313938343135)", + "0-1:24.2.1(170108160000W)(01234.000*m3)" + ] }, "metadata": { "dsmrVersion": 4.2, diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index 6eb99c3..e7eeb92 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -6,8 +6,16 @@ "z": "5" }, "unknownLines": [ + "1-0:72:32.0(00000)" + ], + "crc": { + "value": 52860, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)", - "1-0:72:32.0(00000)", "1-0:31.7.0.255(001*A)", "1-0:51.7.0.255(002*A)", "1-0:71.7.0.255(003*A)", @@ -18,14 +26,30 @@ "1-0:42.7.0.255(05.555*kW)", "1-0:62.7.0.255(06.666*kW)" ], - "crc": { - "value": 52860, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "1-3:0.2.8(42)", + "0-0:1.0.0(101209113020W)", + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(123456.789*kWh)", + "1-0:1.8.2(123456.789*kWh)", + "1-0:2.8.1(123456.789*kWh)", + "1-0:2.8.2(123456.789*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(01.193*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00002)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00003)", + "1-0:72.36.0(00000)", + "0-0:96.13.1(3031203631203831)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "0-1:24.1.0(003)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.2.1(101209110000W)(12785.123*m3)" + ] }, "metadata": { "dsmrVersion": 4.2, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index bd69e65..73207c0 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -5,17 +5,48 @@ "xxx": "Ene", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)" - ], "crc": { "value": 54685, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(9)(0-0:96.7.19)(190221201823W)(0000012287*s)(190221165316W)(0000066621*s)(190220173949W)(0000240199*s)(190217224923W)(0000009884*s)(190217200128W)(0000000813*s)(190217194527W)(0000005140*s)(190217181705W)(0000000266*s)(190217180549W)(0000331071*s)(190213220245W)(0000000230*s)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(210330192305S)", + "0-0:96.1.1(serienummer)", + "1-0:1.8.1(000009.533*kWh)", + "1-0:1.8.2(000014.154*kWh)", + "1-0:2.8.1(002248.911*kWh)", + "1-0:2.8.2(005072.177*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(00.566*kW)", + "0-0:96.7.21(00220)", + "0-0:96.7.9(00034)", + "1-0:32.32.0(00024)", + "1-0:52.32.0(00024)", + "1-0:72.32.0(00024)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "0-0:96.13.0()", + "1-0:32.7.0(226.0*V)", + "1-0:52.7.0(227.0*V)", + "1-0:72.7.0(226.0*V)", + "1-0:31.7.0(004*A)", + "1-0:51.7.0(004*A)", + "1-0:71.7.0(004*A)", + "1-0:21.7.0(00.000*kW)", + "1-0:41.7.0(00.000*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:22.7.0(00.068*kW)", + "1-0:42.7.0(00.240*kW)", + "1-0:62.7.0(00.257*kW)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index fd69396..ca5eb23 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -5,17 +5,51 @@ "xxx": "Ene", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)" - ], "crc": { "value": 45141, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(1)(0-0:96.7.19)(171024204625S)(0000000305*s)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(180108202537W)", + "0-0:96.1.1(serienummer)", + "1-0:1.8.1(000000.855*kWh)", + "1-0:1.8.2(000000.693*kWh)", + "1-0:2.8.1(000000.084*kWh)", + "1-0:2.8.2(000000.000*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(00.134*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00008)", + "0-0:96.7.9(00004)", + "1-0:32.32.0(00003)", + "1-0:52.32.0(00003)", + "1-0:72.32.0(00002)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "0-0:96.13.0()", + "1-0:32.7.0(229.0*V)", + "1-0:52.7.0(226.0*V)", + "1-0:72.7.0(229.0*V)", + "1-0:31.7.0(000*A)", + "1-0:51.7.0(000*A)", + "1-0:71.7.0(000*A)", + "1-0:21.7.0(00.094*kW)", + "1-0:41.7.0(00.040*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:22.7.0(00.000*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:62.7.0(00.000*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(serienummer)", + "0-1:24.2.1(180108205500W)(00001.290*m3)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index ed88d52..5aeed04 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -5,18 +5,51 @@ "xxx": "Ene", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(0)(0-0:96.7.19)", - "1-0:42.1.0(00.000*kW)" - ], "crc": { "value": 46984, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(0)(0-0:96.7.19)", + "1-0:42.1.0(00.000*kW)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(240220170958W)", + "0-0:96.1.1(4530303632303030303134353236323233)", + "1-0:1.8.1(000565.971*kWh)", + "1-0:1.8.2(000694.269*kWh)", + "1-0:2.8.1(000006.754*kWh)", + "1-0:2.8.2(000007.849*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(00.723*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00010)", + "0-0:96.7.9(00003)", + "1-0:32.32.0(00001)", + "1-0:52.32.0(00001)", + "1-0:72.32.0(00001)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "0-0:96.13.0()", + "1-0:32.7.0(226.0*V)", + "1-0:52.7.0(225.0*V)", + "1-0:72.7.0(226.0*V)", + "1-0:31.7.0(003*A)", + "1-0:51.7.0(000*A)", + "1-0:71.7.0(000*A)", + "1-0:21.7.0(00.654*kW)", + "1-0:41.7.0(00.069*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:62.7.0(00.000*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(4730303732303033393634343938373139)", + "0-1:24.2.1(240220171000W)(06362.120*m3)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index 3c904fb..cafd142 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -5,7 +5,13 @@ "xxx": "EST", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 65535, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "1-0:3.8.0(000036520*varh)", "1-0:3.8.1(000032377*varh)", "1-0:3.8.2(000004143*varh)", @@ -15,14 +21,18 @@ "1-0:4.8.2(000090184*varh)", "1-0:4.7.0(000000221*var)" ], - "crc": { - "value": 65535, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(250319102812W)", + "1-0:1.8.0(000201282*Wh)", + "1-0:1.8.1(000134904*Wh)", + "1-0:1.8.2(000066378*Wh)", + "1-0:1.7.0(000000000*W)", + "1-0:2.8.0(000320908*Wh)", + "1-0:2.8.1(000317131*Wh)", + "1-0:2.8.2(000003777*Wh)", + "1-0:2.7.0(000003996*W)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index edd117a..cd8714f 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -5,17 +5,51 @@ "xxx": "ISK", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)" - ], "crc": { "value": 7976, "valid": true } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(1)(0-0:96.7.19)(180529135630S)(0000002451*s)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(181106140429W)", + "0-0:96.1.1(4530303334303036383130353136343136)", + "1-0:1.8.1(003808.351*kWh)", + "1-0:1.8.2(002948.827*kWh)", + "1-0:2.8.1(001285.951*kWh)", + "1-0:2.8.2(002876.514*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(00.498*kW)", + "0-0:96.7.21(00006)", + "0-0:96.7.9(00003)", + "1-0:32.32.0(00003)", + "1-0:52.32.0(00002)", + "1-0:72.32.0(00002)", + "1-0:32.36.0(00001)", + "1-0:52.36.0(00001)", + "1-0:72.36.0(00001)", + "0-0:96.13.0()", + "1-0:32.7.0(236.0*V)", + "1-0:52.7.0(232.6*V)", + "1-0:72.7.0(235.1*V)", + "1-0:31.7.0(002*A)", + "1-0:51.7.0(000*A)", + "1-0:71.7.0(000*A)", + "1-0:21.7.0(00.000*kW)", + "1-0:41.7.0(00.033*kW)", + "1-0:61.7.0(00.132*kW)", + "1-0:22.7.0(00.676*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:62.7.0(00.000*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(4730303339303031373030343630313137)", + "0-1:24.2.1(181106140010W)(01569.646*m3)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index e11724d..0d43c2f 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -5,17 +5,51 @@ "xxx": "ISk", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" - ], "crc": { "value": 61231, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(101209113020W)", + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(123456.789*kwh)", + "1-0:1.8.2(123456.789*kwh)", + "1-0:2.8.1(123456.789*kwh)", + "1-0:2.8.2(123456.789*kwh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(01.193*kw)", + "1-0:2.7.0(00.000*kw)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00002)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:72.32.0(00000)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00003)", + "1-0:72.36.0(00000)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "1-0:32.7.0(220.1*v)", + "1-0:52.7.0(220.2*v)", + "1-0:72.7.0(220.3*v)", + "1-0:31.7.0(001*a)", + "1-0:51.7.0(002*a)", + "1-0:71.7.0(003*a)", + "1-0:21.7.0(01.111*kw)", + "1-0:41.7.0(02.222*kw)", + "1-0:61.7.0(03.333*kw)", + "1-0:22.7.0(04.444*kw)", + "1-0:42.7.0(05.555*kw)", + "1-0:62.7.0(06.666*kw)", + "0-1:24.1.0(003)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.2.1(101209112500W)(12785.123*m3)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index e11724d..32ecf5c 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -5,17 +5,51 @@ "xxx": "ISk", "z": "5" }, - "unknownLines": [ - "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" - ], "crc": { "value": 61231, "valid": false } }, "cosem": { - "unknownObjects": [], - "knownObjects": [] + "unknownObjects": [ + "1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)" + ], + "knownObjects": [ + "1-3:0.2.8(50)", + "0-0:1.0.0(101209113020W)", + "0-0:96.1.1(4B384547303034303436333935353037)", + "1-0:1.8.1(123456.789*kWh)", + "1-0:1.8.2(123456.789*kWh)", + "1-0:2.8.1(123456.789*kWh)", + "1-0:2.8.2(123456.789*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(01.193*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00004)", + "0-0:96.7.9(00002)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:72.32.0(00000)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00003)", + "1-0:72.36.0(00000)", + "0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F)", + "1-0:32.7.0(220.1*V)", + "1-0:52.7.0(220.2*V)", + "1-0:72.7.0(220.3*V)", + "1-0:31.7.0(001*A)", + "1-0:51.7.0(002*A)", + "1-0:71.7.0(003*A)", + "1-0:21.7.0(01.111*kW)", + "1-0:41.7.0(02.222*kW)", + "1-0:61.7.0(03.333*kW)", + "1-0:22.7.0(04.444*kW)", + "1-0:42.7.0(05.555*kW)", + "1-0:62.7.0(06.666*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(3232323241424344313233343536373839)", + "0-1:24.2.1(101209112500W)(12785.123*m3)" + ] }, "metadata": { "dsmrVersion": 5, diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index 3e531bf..ebb3a81 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -5,7 +5,13 @@ "xxx": "Lux", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 35666, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "0-0:42.0.0(53414731303330373930303032353734)", "1-0:3.8.0(000000.835*kvarh)", "1-0:4.8.0(000063.781*kvarh)", @@ -33,14 +39,46 @@ "0-3:24.4.0(1)", "0-4:24.4.0(1)" ], - "crc": { - "value": 35666, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "1-3:0.2.8(42)", + "0-0:1.0.0(200706104157S)", + "1-0:1.8.0(000025.653*kWh)", + "1-0:2.8.0(000000.040*kWh)", + "1-0:1.7.0(00.005*kW)", + "1-0:2.7.0(00.000*kW)", + "0-0:96.7.21(00099)", + "1-0:32.32.0(00040)", + "1-0:52.32.0(00003)", + "1-0:72.32.0(00002)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "0-0:96.13.0()", + "1-0:32.7.0(233.0*V)", + "1-0:52.7.0(000.0*V)", + "1-0:72.7.0(001.0*V)", + "1-0:31.7.0(000*A)", + "1-0:51.7.0(000*A)", + "1-0:71.7.0(000*A)", + "1-0:21.7.0(00.005*kW)", + "1-0:41.7.0(00.000*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:22.7.0(00.000*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:62.7.0(00.000*kW)", + "0-1:24.1.0(003)", + "0-1:96.1.0(464C4F313839393030303630333535)", + "0-1:24.2.1(200706103140S)(00000.006*m3)", + "0-2:24.1.0(007)", + "0-2:96.1.0()", + "0-2:24.2.1(632525252525S)(00000.000)", + "0-3:24.1.0(007)", + "0-3:96.1.0()", + "0-3:24.2.1(632525252525S)(00000.000)", + "0-4:24.1.0(003)", + "0-4:96.1.0(454C53333533353839393830333030)", + "0-4:24.2.1(200706102900S)(00028.103*m3)" + ] }, "metadata": { "dsmrVersion": 4.2, diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json index f7c1b86..917e00a 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json @@ -5,7 +5,13 @@ "xxx": "FLU", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 4660, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "0-0:96.1.4(50221)", "1-0:94.32.1(400)", "0-0:96.1.2(353431343430303132333435363738393030)", @@ -25,14 +31,34 @@ "0-2:96.1.1(3853414731323334353637383930)", "0-2:96.1.2(353431343430303132333435363738393033)" ], - "crc": { - "value": 4660, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "0-0:96.1.1(3153414733313031303231363035)", + "0-0:1.0.0(200512135409S)", + "1-0:1.8.1(000000.034*kWh)", + "1-0:1.8.2(000015.758*kWh)", + "1-0:2.8.1(000000.000*kWh)", + "1-0:2.8.2(000000.011*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(00.000*kW)", + "1-0:21.7.0(00.000*kW)", + "1-0:41.7.0(00.000*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:22.7.0(00.000*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:62.7.0(00.000*kW)", + "1-0:32.7.0(234.7*V)", + "1-0:52.7.0(234.7*V)", + "1-0:72.7.0(234.7*V)", + "1-0:31.7.0(000.00*A)", + "1-0:51.7.0(000.00*A)", + "1-0:71.7.0(000.00*A)", + "0-0:96.13.0()", + "0-1:24.1.0(003)", + "0-1:24.2.3(200512134558S)(00112.384*m3)", + "0-2:24.1.0(007)", + "0-2:24.2.3(200512134558S)(00872.234*m3)" + ] }, "metadata": { "equipmentId": "3153414733313031303231363035", diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json index a48e8a7..3b29f82 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json @@ -5,7 +5,13 @@ "xxx": "FLU", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 4660, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "0-0:96.1.4(50221)", "0-0:96.1.2(353431343430303132333435363738393030)", "1-0:1.4.0(02.351*kW)", @@ -24,14 +30,26 @@ "0-2:96.1.1(3853414731323334353637383930)", "0-2:96.1.2(353431343430303132333435363738393033)" ], - "crc": { - "value": 4660, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "0-0:96.1.1(3153414731313030303030323331)", + "0-0:1.0.0(200512145552S)", + "1-0:1.8.1(000000.915*kWh)", + "1-0:1.8.2(000001.955*kWh)", + "1-0:2.8.1(000000.000*kWh)", + "1-0:2.8.2(000000.030*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(00.000*kW)", + "1-0:2.7.0(00.000*kW)", + "1-0:21.7.0(00.000*kW)", + "1-0:22.7.0(00.000*kW)", + "1-0:32.7.0(234.6*V)", + "1-0:31.7.0(000.00*A)", + "0-0:96.13.0()", + "0-1:24.1.0(003)", + "0-1:24.2.3(200512134558S)(00112.384*m3)", + "0-2:24.1.0(007)", + "0-2:24.2.3(200512134558S)(00872.234*m3)" + ] }, "metadata": { "equipmentId": "3153414731313030303030323331", diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json index c21e4f5..b1e502a 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json @@ -6,16 +6,31 @@ "z": "5" }, "unknownLines": [ + "(13032.850)" + ] + }, + "cosem": { + "unknownObjects": [ "0-0:17.0.0(0999.00*kW)", "0-0:96.3.10(1)", - "(13032.850)", "0-1:24.4.0(1)" + ], + "knownObjects": [ + "0-0:96.1.1(00112233445566778899aabbccddeeff)", + "1-0:1.8.1(39837.604*kWh)", + "1-0:1.8.2(30477.225*kWh)", + "1-0:2.8.1(05174.479*kWh)", + "1-0:2.8.2(11772.946*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(0000.00*kW)", + "1-0:2.7.0(0000.14*kW)", + "0-0:96.13.1()", + "0-0:96.13.0(test-/-test)", + "0-1:24.1.0(3)", + "0-1:96.1.0(0011223344556677889900112233445566)", + "0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)" ] }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] - }, "metadata": { "equipmentId": "00112233445566778899aabbccddeeff", "numericMessage": 0, diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json index 420ddca..2730886 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json @@ -6,16 +6,31 @@ "z": "5" }, "unknownLines": [ + "(13032.850)" + ] + }, + "cosem": { + "unknownObjects": [ "0-0:17.0.0(0999.00*kW)", "0-0:96.3.10(1)", - "(13032.850)", "0-1:24.4.0(1)" + ], + "knownObjects": [ + "0-0:96.1.1(00112233445566778899aabbccddeeff)", + "1-0:1.8.1(39837.604*kWh)", + "1-0:1.8.2(30477.225*kWh)", + "1-0:2.8.1(05174.479*kWh)", + "1-0:2.8.2(11772.946*kWh)", + "0-0:96.14.0(0002)", + "1-0:1.7.0(0000.00*kW)", + "1-0:2.7.0(0000.14*kW)", + "0-0:96.13.1()", + "0-0:96.13.0()", + "0-1:24.1.0(3)", + "0-1:96.1.0(0011223344556677889900112233445566)", + "0-1:24.3.0(250423090000)(00)(60)(1)(0-1:24.2.1)(m3)" ] }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] - }, "metadata": { "equipmentId": "00112233445566778899aabbccddeeff", "numericMessage": 0, diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index 77c5888..c62efe8 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -5,7 +5,13 @@ "xxx": "KAM", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 15096, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "1-0:3.8.0(00001234.432*kVArh)", "1-0:4.8.0(00005678.765*kVArh)", "1-0:3.7.0(0001.229*kVAr)", @@ -17,14 +23,25 @@ "1-0:44.7.0(0000.000*kVAr)", "1-0:64.7.0(0000.000*kVAr)" ], - "crc": { - "value": 15096, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "0-0:1.0.0(250000000000W)", + "1-0:1.8.0(00000123.321*kWh)", + "1-0:2.8.0(00000456.654*kWh)", + "1-0:1.7.0(0002.424*kW)", + "1-0:2.7.0(0000.000*kW)", + "1-0:21.7.0(0000.682*kW)", + "1-0:41.7.0(0000.750*kW)", + "1-0:61.7.0(0000.992*kW)", + "1-0:22.7.0(0000.000*kW)", + "1-0:42.7.0(0000.000*kW)", + "1-0:62.7.0(0000.000*kW)", + "1-0:32.7.0(225.4*V)", + "1-0:52.7.0(229.4*V)", + "1-0:72.7.0(225.9*V)", + "1-0:31.7.0(003.4*A)", + "1-0:51.7.0(003.9*A)", + "1-0:71.7.0(004.6*A)" + ] }, "metadata": { "timestamp": "250000000000W" diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index 104af03..8f83222 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -5,7 +5,13 @@ "xxx": "GRE", "z": "5" }, - "unknownLines": [ + "crc": { + "value": 15269, + "valid": false + } + }, + "cosem": { + "unknownObjects": [ "0-0:0.0.0(123412341234)", "1-0:99.97.0(2)(0-0:96.7.19)(240523144934S)(0005564711*s)(241109083138W)(0000000124*s)", "0-0:96.1.4(12345)", @@ -14,14 +20,38 @@ "0-0:98.1.0(12)", "1-0:1.4.0(00.288*kW)" ], - "crc": { - "value": 15269, - "valid": false - } - }, - "cosem": { - "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "0-0:1.0.0(123412341234W)", + "1-1:1.8.1(001595.070*kWh)", + "1-1:1.8.2(005216.778*kWh)", + "1-1:2.8.1(004478.038*kWh)", + "1-1:2.8.2(000004.648*kWh)", + "0-0:96.14.0(0001)", + "1-1:1.7.0(01.622*kW)", + "1-1:2.7.0(00.000*kW)", + "0-0:96.7.21(00018)", + "0-0:96.7.9(00004)", + "1-0:32.32.0(00002)", + "1-0:52.32.0(00001)", + "1-0:72.32.0(00001)", + "1-0:32.36.0(00000)", + "1-0:52.36.0(00000)", + "1-0:72.36.0(00000)", + "1-0:32.7.0(238.9*V)", + "1-0:52.7.0(231.9*V)", + "1-0:72.7.0(239.7*V)", + "1-0:31.7.0(005.15*A)", + "1-0:51.7.0(017.19*A)", + "1-0:71.7.0(005.02*A)", + "0-0:96.13.0()", + "1-0:21.7.0(00.000*kW)", + "1-0:22.7.0(01.175*kW)", + "1-0:41.7.0(03.946*kW)", + "1-0:42.7.0(00.000*kW)", + "1-0:61.7.0(00.000*kW)", + "1-0:62.7.0(01.148*kW)", + "0-0:96.13.1()" + ] }, "metadata": { "timestamp": "123412341234W", diff --git a/tests/telegrams/dsmr/unknown-xmx-1.json b/tests/telegrams/dsmr/unknown-xmx-1.json index 4b6cc44..cdf6c1f 100644 --- a/tests/telegrams/dsmr/unknown-xmx-1.json +++ b/tests/telegrams/dsmr/unknown-xmx-1.json @@ -8,7 +8,18 @@ }, "cosem": { "unknownObjects": [], - "knownObjects": [] + "knownObjects": [ + "0-0:96.1.1(0123456789ABCDEF)", + "1-0:1.8.1(11667.440*kWh)", + "1-0:1.8.2(11781.558*kWh)", + "1-0:2.8.1(00000.000*kWh)", + "1-0:2.8.2(00000.000*kWh)", + "0-0:96.14.0(0001)", + "1-0:1.7.0(0000.55*kW)", + "1-0:2.7.0(0000.00*kW)", + "0-0:96.13.1()", + "0-0:96.13.0()" + ] }, "metadata": { "equipmentId": "0123456789ABCDEF", From 35dd18ff32801e67b7bef7188c9f6319a29bf0a5 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 27 May 2025 11:20:05 +0200 Subject: [PATCH 09/18] chore: fix some todo's and add described list test case --- src/protocols/dlms-datatype.ts | 64 ++++++--------- src/protocols/dlms-payload/DescribedList.ts | 8 +- src/protocols/hdlc.ts | 2 - src/stream/stream-dlms.ts | 14 +--- src/stream/stream-encrypted-dsmr.ts | 2 +- tests/telegrams/dlms/described-list.json | 86 +++++++++++++++++++++ tests/telegrams/dlms/described-list.txt | 44 +++++++++++ 7 files changed, 161 insertions(+), 59 deletions(-) create mode 100644 tests/telegrams/dlms/described-list.json create mode 100644 tests/telegrams/dlms/described-list.txt diff --git a/src/protocols/dlms-datatype.ts b/src/protocols/dlms-datatype.ts index 7eed202..69fd546 100644 --- a/src/protocols/dlms-datatype.ts +++ b/src/protocols/dlms-datatype.ts @@ -127,6 +127,27 @@ class DlmsDataTypesInternal { } } +const parseStructureOrArray = (index: number, buffer: Buffer) => { + const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); + index = newIndex; + + const resultValue: DlmsDataTypes['array'] = []; + + for (let i = 0; i < objectCount; i++) { + const { value, index: newIndex, type } = DlmsDataTypes.parse(buffer, index); + index = newIndex; + resultValue.push({ + value, + type, + }); + } + + return { + index, + value: resultValue, + }; +}; + // TODO: We need to add all data types, because otherwise // we will get an error when we try to parse a data type we don't know. /** @@ -137,47 +158,8 @@ class DlmsDataTypesInternal { * - The value (length is either determined by the tag and length) */ export const DlmsDataTypes = new DlmsDataTypesInternal() - .addDataType('array', 0x01, (index, buffer) => { - const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); - index = newIndex; - - const resultValue: DlmsDataTypes['array'] = []; - - for (let i = 0; i < objectCount; i++) { - const { value, index: newIndex, type } = DlmsDataTypes.parse(buffer, index); - index = newIndex; - resultValue.push({ - value, - type, - }); - } - - return { - index, - value: resultValue, - }; - }) - .addDataType('structure', 0x02, (index, buffer) => { - // TODO: This is the same as array - const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); - index = newIndex; - - const resultValue: DlmsDataTypes['array'] = []; - - for (let i = 0; i < objectCount; i++) { - const { value, index: newIndex, type } = DlmsDataTypes.parse(buffer, index); - index = newIndex; - resultValue.push({ - value, - type, - }); - } - - return { - index, - value: resultValue, - }; - }) + .addDataType('array', 0x01, parseStructureOrArray) + .addDataType('structure', 0x02, parseStructureOrArray) .addDataType('octet_string', 0x09, (index, buffer) => { const { objectCount, newIndex } = getDlmsObjectCount(buffer, index); index = newIndex; diff --git a/src/protocols/dlms-payload/DescribedList.ts b/src/protocols/dlms-payload/DescribedList.ts index cf4d992..ee44c19 100644 --- a/src/protocols/dlms-payload/DescribedList.ts +++ b/src/protocols/dlms-payload/DescribedList.ts @@ -1,5 +1,5 @@ import { getDlmsObisCode, isDlmsStructureLike } from '../dlms-datatype.js'; -import { makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; +import { addUnknownDlmsCosemObject, addUnknownDlmsObject, makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; export const DlmsPayloadDescribedList = makeDlmsPayload('DescribedList', { detector(dlms) { @@ -30,29 +30,33 @@ export const DlmsPayloadDescribedList = makeDlmsPayload('DescribedList', { for (const [index, descriptor] of descriptorList.value.entries()) { if (!isDlmsStructureLike(descriptor)) { - // TODO: Add unknown object. + addUnknownDlmsObject(descriptor, result); continue; } const obisRaw = descriptor.value[1]; if (!obisRaw) { + addUnknownDlmsObject(descriptor, result); continue; } const obisCode = getDlmsObisCode(obisRaw); if (!obisCode) { + addUnknownDlmsObject(descriptor, result); continue; } const valueRaw = dlms.value[index + 1]; if (!valueRaw) { + addUnknownDlmsCosemObject(obisCode, descriptor, result); continue; } if (typeof valueRaw.value !== 'string' && typeof valueRaw.value !== 'number') { + addUnknownDlmsCosemObject(obisCode, valueRaw, result); continue; } diff --git a/src/protocols/hdlc.ts b/src/protocols/hdlc.ts index 6147df3..0ff458a 100644 --- a/src/protocols/hdlc.ts +++ b/src/protocols/hdlc.ts @@ -124,8 +124,6 @@ export const decodeHdlcHeader = (data: Buffer) => { const format = data.readUint8(index++); const formatType = (format >> 4) & 0b1111; - // TODO: Is this bit "Segmentation supported", or "This frame is segmented"? - // The control bit also has a final bit, maybe that is used to indicate the end of a segmented frame? const segmentation = (format & 0x08) !== 0; if (formatType !== HDLC_FORMAT_START) { diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index 200f542..b91b308 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -84,8 +84,6 @@ export class DlmsStreamParser implements SmartMeterStreamParser { this.header = decodeHdlcHeader(this.telegram); this.headers.push(this.header); } catch (rawError) { - this.clear(); - const error = toSmartMeterError(rawError); if (error instanceof SmartMeterError) { @@ -94,17 +92,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { this.options.callback(error); - // TODO: This seems weird, I've just cleared the buffer so this.telegram should be empty... - const remainingData = this.telegram.subarray(1, this.telegram.length); - this.hasStartOfFrame = false; - this.header = undefined; - this.telegram = Buffer.alloc(0); - this.cachedContent = Buffer.alloc(0); - - // There might be more data in the buffer for the next telegram. - if (remainingData.length > 0) { - this.onData(remainingData); - } + this.clear(); return; } } diff --git a/src/stream/stream-encrypted-dsmr.ts b/src/stream/stream-encrypted-dsmr.ts index a20790f..7fb5dfa 100644 --- a/src/stream/stream-encrypted-dsmr.ts +++ b/src/stream/stream-encrypted-dsmr.ts @@ -74,9 +74,9 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { try { this.header = decodeEncryptionHeader(this.telegram); } catch (err) { - this.clear(); const error = toSmartMeterError(err); error.withRawTelegram(this.telegram); + this.clear(); this.options.callback(error); return; diff --git a/tests/telegrams/dlms/described-list.json b/tests/telegrams/dlms/described-list.json new file mode 100644 index 0000000..0f3b05a --- /dev/null +++ b/tests/telegrams/dlms/described-list.json @@ -0,0 +1,86 @@ +{ + "hdlc": { + "headers": [ + { + "destinationAddress": 1, + "sourceAddress": 2, + "crc": { + "valid": true, + "value": 45603 + } + } + ], + "footers": [ + { + "crc": { + "valid": false, + "value": 55223 + } + } + ], + "crc": { + "valid": false + } + }, + "dlms": { + "invokeId": 0, + "timestamp": "", + "unknownObjects": [ + "0-6:25.9.0(0006190900ff)", + "0-6:25.9.0(00112233445566778899aabbccddeeff)", + "0-0:42.0.0(aabbccddeeffee)", + "0-0:96.1.1(ffeeddccbbaa998877665544)", + "1-1:8.8.0(null)", + "1-1:8.8.1(null)", + "1-1:8.8.2(null)", + "1-0:13.7.0([{\"value\":3,\"type\":\"uint16\"},{\"value\":{\"type\":\"Buffer\",\"data\":[1,0,13,7,0,255]},\"type\":\"octet_string\"},{\"value\":2,\"type\":\"int8\"},{\"value\":0,\"type\":\"uint16\"}])" + ], + "payloadType": "DescribedList" + }, + "cosem": { + "unknownObjects": [ + "0-0:1.0.0(143)", + "1-1:3.7.0(205)", + "1-1:4.7.0(19809717)", + "1-1:5.8.0(78078)", + "1-1:5.8.1(77822)", + "1-1:5.8.2(0)", + "1-1:6.8.0(0)", + "1-1:6.8.1(0)", + "1-1:6.8.2(6)", + "1-1:7.8.0(3)", + "1-1:7.8.1(3)", + "1-1:7.8.2(6456638)" + ], + "knownObjects": [ + "1-1:1.7.0(0)", + "1-1:2.7.0(0)", + "1-1:1.8.0(11164972)", + "1-1:1.8.1(8644745)", + "1-1:1.8.2(4)", + "1-1:2.8.0(1)", + "1-1:2.8.1(3)", + "1-1:2.8.2(155900)" + ] + }, + "electricity": { + "powerReceivedTotal": 0, + "powerReturnedTotal": 0, + "total": { + "received": 11164972, + "returned": 1 + }, + "tariffs": { + "1": { + "received": 8644745, + "returned": 3 + }, + "2": { + "received": 4, + "returned": 155900 + } + } + }, + "mBus": {}, + "metadata": {} +} \ No newline at end of file diff --git a/tests/telegrams/dlms/described-list.txt b/tests/telegrams/dlms/described-list.txt new file mode 100644 index 0000000..4d51f52 --- /dev/null +++ b/tests/telegrams/dlms/described-list.txt @@ -0,0 +1,44 @@ +7e a2 af 03 05 00 23 b2 e6 e7 00 0f 00 01 7e 9c +0c 07 e9 05 14 02 09 2b 2d 00 ff 88 80 02 1c 01 +1c 02 04 12 00 28 09 06 00 06 19 09 00 ff 0f 02 +12 00 00 02 04 12 00 28 09 06 00 06 19 09 00 ff +0f 01 12 00 00 02 04 12 00 01 09 06 00 00 2a 00 +00 ff 0f 02 12 00 00 02 04 12 00 01 09 06 00 00 +60 01 01 ff 0f 02 12 00 00 02 04 12 00 08 09 06 +00 00 01 00 00 ff 0f 02 12 00 00 02 04 12 00 03 +09 06 01 01 01 07 00 ff 0f 02 12 00 00 02 04 12 +00 03 09 06 01 01 02 07 00 ff 0f 02 12 00 00 02 +04 12 00 03 09 06 01 01 03 07 00 ff 0f 02 12 00 +00 02 04 12 00 03 09 06 01 01 04 07 00 ff 0f 02 +12 00 00 02 04 12 00 03 09 06 01 01 01 08 00 ff +0f 02 12 00 00 02 04 12 00 03 09 06 01 01 01 08 +01 ff 0f 02 12 00 00 02 04 12 00 03 09 06 01 01 +01 08 02 ff 0f 02 12 00 00 02 04 12 00 03 09 06 +01 01 02 08 00 ff 0f 02 12 00 00 02 04 12 00 03 +09 06 01 01 02 08 01 ff 0f 02 12 00 00 02 04 12 +00 03 09 06 01 01 02 08 02 ff 0f 02 12 00 00 02 +04 12 00 03 09 06 01 01 05 08 00 ff 0f 02 12 00 +00 02 04 12 00 03 09 06 01 01 05 08 01 ff 0f 02 +12 00 00 02 04 12 00 03 09 06 01 01 05 08 02 ff +0f 02 12 00 00 02 04 12 00 03 09 06 01 01 06 08 +00 ff 0f 02 12 00 00 02 04 12 00 03 09 06 01 01 +06 08 01 ff 0f 02 12 00 00 02 04 12 00 03 09 06 +01 01 06 08 02 ff 0f 02 12 00 00 02 04 12 00 03 +09 06 01 01 07 08 00 ff 0f 02 12 00 00 02 04 12 +00 03 09 06 01 01 07 08 01 ff 0f 02 12 00 00 02 +04 12 00 03 09 06 01 01 07 08 02 ff 0f 02 12 00 +00 02 04 12 00 03 09 06 01 01 08 08 00 ff 0f 02 +12 00 00 02 04 12 00 03 09 06 01 01 08 08 01 ff +0f 02 12 00 00 02 04 12 00 03 09 06 01 01 08 08 +02 ff 0f 02 12 00 00 02 04 12 00 03 09 06 01 00 +0d 07 00 ff 0f 02 12 00 00 09 06 00 06 19 09 00 +ff 09 10 00 11 22 33 44 55 66 77 88 99 aa bb cc +dd ee ff 09 07 aa bb cc dd ee ff ee 09 0c ff ee +dd cc bb aa 99 88 77 66 55 44 06 00 00 00 8f 06 +00 00 00 00 06 00 00 00 00 06 00 00 00 cd 06 01 +2e 45 b5 06 00 aa 5d 2c 06 00 83 e8 89 06 00 00 +00 04 06 00 00 00 01 06 00 00 00 03 06 00 02 60 +fc 06 00 01 30 fe 06 00 01 2f fe 06 00 00 00 00 +06 00 00 00 00 06 00 00 00 00 06 00 00 00 06 06 +00 00 00 03 06 00 00 00 03 06 00 62 85 3e b7 d7 +7e From d992a0ca5a867675b53e3037297a84c068bde8b9 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 08:47:13 +0200 Subject: [PATCH 10/18] feat: add timestamp parsing for dsmr --- src/protocols/cosem.ts | 23 +++++++- src/protocols/dlms-datatype.ts | 4 +- src/protocols/dlms-payload/DescribedList.ts | 7 ++- src/util/base-result.ts | 4 +- tests/stream/stream-dsmr.spec.ts | 57 +++++++++++++++---- tests/telegrams/dsmr/dsmr-2.2-kfm-1.json | 2 +- .../telegrams/dsmr/dsmr-3.0-spec-example.json | 2 +- tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 4 +- tests/telegrams/dsmr/dsmr-4.0-isk-2.json | 2 +- .../telegrams/dsmr/dsmr-4.0-spec-example.json | 4 +- tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 4 +- tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 4 +- .../dsmr/dsmr-4.2.2-spec-example.json | 4 +- tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 4 +- tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 4 +- tests/telegrams/dsmr/dsmr-5.0-est-units.json | 2 +- tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 4 +- .../dsmr/dsmr-5.0-spec-example-lowercase.json | 4 +- .../telegrams/dsmr/dsmr-5.0-spec-example.json | 4 +- .../dsmr/dsmr-luxembourgh-spec-example.json | 6 +- .../dsmr/emucs-p1-v2.1.1-spec-example-1.json | 6 +- .../dsmr/emucs-p1-v2.1.1-spec-example-2.json | 6 +- ...iskra-mt-382-no-crc-with-text-message.json | 2 +- tests/telegrams/dsmr/iskra-mt-382-no-crc.json | 2 +- .../kamstrup-OMNIA-e-meter-three-phase.json | 2 +- tests/telegrams/dsmr/sagemcom-xt211.json | 2 +- 27 files changed, 113 insertions(+), 58 deletions(-) diff --git a/src/protocols/cosem.ts b/src/protocols/cosem.ts index 6139380..ba8f59b 100644 --- a/src/protocols/cosem.ts +++ b/src/protocols/cosem.ts @@ -118,12 +118,29 @@ class CosemLibraryInternal { } } +const parseTimeStamp = (value: string): Date | string => { + // YYMMDDhhmmssX used in DSMR P1 telegrams + // X = 'W' for winter time, 'S' for summer time + const match = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([WS]?)$/.exec(value); + if (match) { + const year = parseInt(match[1], 10) + 2000; // DSMR uses YY, so we add 2000 + const month = parseInt(match[2], 10); + const day = parseInt(match[3], 10); + const hour = parseInt(match[4], 10); + const minute = parseInt(match[5], 10); + const second = parseInt(match[6], 10); + return new Date(Date.UTC(year, month - 1, day, hour, minute, second)); + } + + return value; +}; + export const CosemLibrary = new CosemLibraryInternal() .addNumberParser('1-3:0.2.8', ({ valueNumber, result }) => { result.metadata.dsmrVersion = valueNumber / 10; }) .addStringParser('0-0:1.0.0', ({ valueString, result }) => { - result.metadata.timestamp = valueString; // TODO: Parse to date object + result.metadata.timestamp = parseTimeStamp(valueString); }) .addStringParser('0-0:96.1.1', ({ valueString, result }) => { result.metadata.equipmentId = valueString; @@ -309,7 +326,7 @@ export const CosemLibrary = new CosemLibraryInternal() const unit = match[3]; result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].timestamp = timestamp; // TODO: Parse to date object + result.mBus[busId].timestamp = parseTimeStamp(timestamp); result.mBus[busId].value = mbusValue; result.mBus[busId].unit = unit; }) @@ -348,7 +365,7 @@ export const CosemLibrary = new CosemLibraryInternal() const mbusValue = parseFloat(nextLineMatch[1]); result.mBus[busId] = result.mBus[busId] ?? {}; - result.mBus[busId].timestamp = timestamp; // TODO: Parse to date object + result.mBus[busId].timestamp = parseTimeStamp(timestamp); result.mBus[busId].value = mbusValue; result.mBus[busId].unit = unit; result.mBus[busId].recordingPeriodMinutes = recordingPeriodMinutes; diff --git a/src/protocols/dlms-datatype.ts b/src/protocols/dlms-datatype.ts index 69fd546..6d472e8 100644 --- a/src/protocols/dlms-datatype.ts +++ b/src/protocols/dlms-datatype.ts @@ -148,14 +148,14 @@ const parseStructureOrArray = (index: number, buffer: Buffer) => { }; }; -// TODO: We need to add all data types, because otherwise -// we will get an error when we try to parse a data type we don't know. /** * A DLMS data type is: * * - A tag * - A Length (only for some data types) * - The value (length is either determined by the tag and length) + * + * @note There are more data types. But these are the ones used by smart meters. */ export const DlmsDataTypes = new DlmsDataTypesInternal() .addDataType('array', 0x01, parseStructureOrArray) diff --git a/src/protocols/dlms-payload/DescribedList.ts b/src/protocols/dlms-payload/DescribedList.ts index ee44c19..870dfc6 100644 --- a/src/protocols/dlms-payload/DescribedList.ts +++ b/src/protocols/dlms-payload/DescribedList.ts @@ -1,5 +1,10 @@ import { getDlmsObisCode, isDlmsStructureLike } from '../dlms-datatype.js'; -import { addUnknownDlmsCosemObject, addUnknownDlmsObject, makeDlmsPayload, parseDlmsCosem } from './dlms-payload.js'; +import { + addUnknownDlmsCosemObject, + addUnknownDlmsObject, + makeDlmsPayload, + parseDlmsCosem, +} from './dlms-payload.js'; export const DlmsPayloadDescribedList = makeDlmsPayload('DescribedList', { detector(dlms) { diff --git a/src/util/base-result.ts b/src/util/base-result.ts index 6d56bca..8df59e7 100644 --- a/src/util/base-result.ts +++ b/src/util/base-result.ts @@ -5,7 +5,7 @@ export type BaseParserResult = { }; metadata: { dsmrVersion?: number; - timestamp?: string; // TODO make this a date object + timestamp?: Date | string; equipmentId?: string; events?: { powerFailures?: number; @@ -85,7 +85,7 @@ export type BaseParserResult = { equipmentId?: string; value?: number; unit?: string; - timestamp?: string; // TODO: Parse to date object + timestamp?: Date | string; recordingPeriodMinutes?: number; // DSMR } >; diff --git a/tests/stream/stream-dsmr.spec.ts b/tests/stream/stream-dsmr.spec.ts index 3cb1717..9ba8b89 100644 --- a/tests/stream/stream-dsmr.spec.ts +++ b/tests/stream/stream-dsmr.spec.ts @@ -38,7 +38,7 @@ const assertDecryptedFrameValid = ({ // Thus, it is deleted here. delete parsed.additionalAuthenticatedDataValid; - assert.deepStrictEqual(parsed, expected); + assert.deepStrictEqual(JSON.parse(JSON.stringify(parsed)), expected); }; describe('DSMRStreamParser', () => { @@ -63,7 +63,10 @@ describe('DSMRStreamParser', () => { instance.destroy(); assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); }); @@ -87,10 +90,16 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output1); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output1, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input1)); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output2); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[1].arguments[1])), + output2, + ); assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input2)); }); @@ -131,7 +140,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input + '\0')); }); @@ -235,7 +247,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); }); @@ -260,7 +275,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); context.mock.timers.tick(1000); @@ -270,7 +288,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[1].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input)); }); @@ -296,10 +317,16 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 2); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); assert.deepStrictEqual(callback.mock.calls[1].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[1].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[1].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[1].arguments[2], Buffer.from(input)); context.mock.timers.tick(1000); @@ -309,7 +336,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 3); assert.deepStrictEqual(callback.mock.calls[2].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[2].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[2].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[2].arguments[2], Buffer.from(input)); }); @@ -335,7 +365,10 @@ describe('DSMRStreamParser', () => { assert.deepStrictEqual(callback.mock.calls.length, 1); assert.deepStrictEqual(callback.mock.calls[0].arguments[0], null); - assert.deepStrictEqual(callback.mock.calls[0].arguments[1], output); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(callback.mock.calls[0].arguments[1])), + output, + ); assert.deepStrictEqual(callback.mock.calls[0].arguments[2], Buffer.from(input)); stream.end(); diff --git a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json index 1156614..3d4368b 100644 --- a/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-2.2-kfm-1.json @@ -55,7 +55,7 @@ "1": { "deviceType": 3, "equipmentId": "3238313031453631373038389930337131", - "timestamp": "120517020000", + "timestamp": "2012-05-17T02:00:00.000Z", "value": 124.477, "unit": "m3", "recordingPeriodMinutes": 60 diff --git a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json index e7a0606..b0add66 100644 --- a/tests/telegrams/dsmr/dsmr-3.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-3.0-spec-example.json @@ -55,7 +55,7 @@ "1": { "equipmentId": "3232323241424344313233343536373839", "deviceType": 3, - "timestamp": "090212160000", + "timestamp": "2009-02-12T16:00:00.000Z", "value": 0, "unit": "m3", "recordingPeriodMinutes": 60 diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index f8fa86d..c83f73c 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -47,7 +47,7 @@ }, "metadata": { "dsmrVersion": 4, - "timestamp": "101209113020W", + "timestamp": "2010-12-09T11:30:20.000Z", "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, @@ -84,7 +84,7 @@ "1": { "deviceType": 3, "equipmentId": "3232323241424344313233343536373839", - "timestamp": "101209110000W", + "timestamp": "2010-12-09T11:00:00.000Z", "value": 12785.123, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json index c2d9b4e..e219990 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json @@ -37,7 +37,7 @@ }, "metadata": { "dsmrVersion": 4, - "timestamp": "000101010000W", + "timestamp": "2000-01-01T01:00:00.000Z", "equipmentId": "4530303034303031353931303932323134", "events": { "powerFailures": 23, diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index 000c46d..c9a5791 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -47,7 +47,7 @@ }, "metadata": { "dsmrVersion": 4, - "timestamp": "101209113020W", + "timestamp": "2010-12-09T11:30:20.000Z", "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, @@ -84,7 +84,7 @@ "1": { "deviceType": 3, "equipmentId": "3232323241424344313233343536373839", - "timestamp": "101209110000W", + "timestamp": "2010-12-09T11:00:00.000Z", "value": 12785.123, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index 24e34cd..7de80db 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -51,7 +51,7 @@ }, "metadata": { "dsmrVersion": 4.2, - "timestamp": "180306123056W", + "timestamp": "2018-03-06T12:30:56.000Z", "equipmentId": "4530303033303030303032313234383133", "events": { "powerFailures": 13, @@ -104,7 +104,7 @@ "1": { "deviceType": 3, "equipmentId": "4730303136353631323033353830313133", - "timestamp": "180306120000W", + "timestamp": "2018-03-06T12:00:00.000Z", "value": 5359.919, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index b0ffffe..c92e2c1 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -41,7 +41,7 @@ }, "metadata": { "dsmrVersion": 4.2, - "timestamp": "170108161107W", + "timestamp": "2017-01-08T16:11:07.000Z", "equipmentId": "4530303331303033303031363939353135", "events": { "powerFailures": 4, @@ -84,7 +84,7 @@ "1": { "deviceType": 3, "equipmentId": "4730303139333430323231313938343135", - "timestamp": "170108160000W", + "timestamp": "2017-01-08T16:00:00.000Z", "value": 1234, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index e7eeb92..bd47e83 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 4.2, - "timestamp": "101209113020W", + "timestamp": "2010-12-09T11:30:20.000Z", "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, @@ -90,7 +90,7 @@ "1": { "deviceType": 3, "equipmentId": "3232323241424344313233343536373839", - "timestamp": "101209110000W", + "timestamp": "2010-12-09T11:00:00.000Z", "value": 12785.123, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index 73207c0..93bd1d2 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -50,7 +50,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "210330192305S", + "timestamp": "2021-03-30T19:23:05.000Z", "equipmentId": "serienummer", "events": { "powerFailures": 220, diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index ca5eb23..7395554 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "180108202537W", + "timestamp": "2018-01-08T20:25:37.000Z", "equipmentId": "serienummer", "events": { "powerFailures": 8, @@ -110,7 +110,7 @@ "1": { "deviceType": 3, "equipmentId": "serienummer", - "timestamp": "180108205500W", + "timestamp": "2018-01-08T20:55:00.000Z", "value": 1.29, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index 5aeed04..7ba08a5 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "240220170958W", + "timestamp": "2024-02-20T17:09:58.000Z", "equipmentId": "4530303632303030303134353236323233", "events": { "powerFailures": 10, @@ -109,7 +109,7 @@ "1": { "deviceType": 3, "equipmentId": "4730303732303033393634343938373139", - "timestamp": "240220171000W", + "timestamp": "2024-02-20T17:10:00.000Z", "value": 6362.12, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index cafd142..ff16556 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -36,7 +36,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "250319102812W" + "timestamp": "2025-03-19T10:28:12.000Z" }, "electricity": { "total": { diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index cd8714f..1d21c69 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "181106140429W", + "timestamp": "2018-11-06T14:04:29.000Z", "equipmentId": "4530303334303036383130353136343136", "events": { "powerFailures": 6, @@ -110,7 +110,7 @@ "1": { "deviceType": 3, "equipmentId": "4730303339303031373030343630313137", - "timestamp": "181106140010W", + "timestamp": "2018-11-06T14:00:10.000Z", "value": 1569.646, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index 0d43c2f..3222956 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "101209113020W", + "timestamp": "2010-12-09T11:30:20.000Z", "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, @@ -110,7 +110,7 @@ "1": { "deviceType": 3, "equipmentId": "3232323241424344313233343536373839", - "timestamp": "101209112500W", + "timestamp": "2010-12-09T11:25:00.000Z", "value": 12785.123, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index 32ecf5c..0068d17 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -53,7 +53,7 @@ }, "metadata": { "dsmrVersion": 5, - "timestamp": "101209113020W", + "timestamp": "2010-12-09T11:30:20.000Z", "equipmentId": "4B384547303034303436333935353037", "events": { "powerFailures": 4, @@ -110,7 +110,7 @@ "1": { "deviceType": 3, "equipmentId": "3232323241424344313233343536373839", - "timestamp": "101209112500W", + "timestamp": "2010-12-09T11:25:00.000Z", "value": 12785.123, "unit": "m3" } diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index ebb3a81..6764d09 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -82,7 +82,7 @@ }, "metadata": { "dsmrVersion": 4.2, - "timestamp": "200706104157S", + "timestamp": "2020-07-06T10:41:57.000Z", "events": { "powerFailures": 99, "voltageSags": { @@ -130,7 +130,7 @@ "1": { "deviceType": 3, "equipmentId": "464C4F313839393030303630333535", - "timestamp": "200706103140S", + "timestamp": "2020-07-06T10:31:40.000Z", "value": 0.006, "unit": "m3" }, @@ -145,7 +145,7 @@ "4": { "deviceType": 3, "equipmentId": "454C53333533353839393830333030", - "timestamp": "200706102900S", + "timestamp": "2020-07-06T10:29:00.000Z", "value": 28.103, "unit": "m3" } diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json index 917e00a..5cc2e31 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json @@ -62,7 +62,7 @@ }, "metadata": { "equipmentId": "3153414733313031303231363035", - "timestamp": "200512135409S", + "timestamp": "2020-05-12T13:54:09.000Z", "textMessage": "" }, "electricity": { @@ -103,13 +103,13 @@ "mBus": { "1": { "deviceType": 3, - "timestamp": "200512134558S", + "timestamp": "2020-05-12T13:45:58.000Z", "value": 112.384, "unit": "m3" }, "2": { "deviceType": 7, - "timestamp": "200512134558S", + "timestamp": "2020-05-12T13:45:58.000Z", "value": 872.234, "unit": "m3" } diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json index 3b29f82..a27865f 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json @@ -53,7 +53,7 @@ }, "metadata": { "equipmentId": "3153414731313030303030323331", - "timestamp": "200512145552S", + "timestamp": "2020-05-12T14:55:52.000Z", "textMessage": "" }, "electricity": { @@ -86,13 +86,13 @@ "mBus": { "1": { "deviceType": 3, - "timestamp": "200512134558S", + "timestamp": "2020-05-12T13:45:58.000Z", "value": 112.384, "unit": "m3" }, "2": { "deviceType": 7, - "timestamp": "200512134558S", + "timestamp": "2020-05-12T13:45:58.000Z", "value": 872.234, "unit": "m3" } diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json index b1e502a..54b9931 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc-with-text-message.json @@ -55,7 +55,7 @@ "1": { "deviceType": 3, "equipmentId": "0011223344556677889900112233445566", - "timestamp": "250423090000", + "timestamp": "2025-04-23T09:00:00.000Z", "value": 13032.85, "unit": "m3", "recordingPeriodMinutes": 60 diff --git a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json index 2730886..8e1101b 100644 --- a/tests/telegrams/dsmr/iskra-mt-382-no-crc.json +++ b/tests/telegrams/dsmr/iskra-mt-382-no-crc.json @@ -55,7 +55,7 @@ "1": { "deviceType": 3, "equipmentId": "0011223344556677889900112233445566", - "timestamp": "250423090000", + "timestamp": "2025-04-23T09:00:00.000Z", "value": 13032.85, "unit": "m3", "recordingPeriodMinutes": 60 diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index c62efe8..3cfd7f1 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -44,7 +44,7 @@ ] }, "metadata": { - "timestamp": "250000000000W" + "timestamp": "2024-11-30T00:00:00.000Z" }, "electricity": { "total": { diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index 8f83222..db7369a 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -54,7 +54,7 @@ ] }, "metadata": { - "timestamp": "123412341234W", + "timestamp": "2014-10-13T10:12:34.000Z", "events": { "powerFailures": 18, "longPowerFailures": 4, From 63ae403131df95a31618e207622c88ba2d52bfef Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:23:03 +0200 Subject: [PATCH 11/18] feat: add initialData and update examples --- README.md | 8 +- examples/homey-energy-dongle-usb.js | 83 +++++--- examples/homey-energy-dongle-ws.js | 93 +++++++-- src/index.ts | 1 + src/stream/stream-detect-type.ts | 4 +- src/stream/stream-dlms.ts | 8 + src/stream/stream-encrypted-dsmr.ts | 8 + src/stream/stream-unencrypted-dsmr.ts | 4 + tests/stream/stream-detect-type.spec.ts | 22 +- .../encrypted/aidon-example-2-with-aad.txt | 76 +++---- .../encrypted/aidon-example-2-without-aad.txt | 76 +++---- ...dsmr-luxembourgh-spec-example-with-aad.txt | 188 +++++++++--------- ...r-luxembourgh-spec-example-without-aad.txt | 188 +++++++++--------- tests/test-utils.ts | 2 +- tsconfig.json | 2 +- 15 files changed, 436 insertions(+), 327 deletions(-) diff --git a/README.md b/README.md index b4a74e2..e9d49fb 100644 --- a/README.md +++ b/README.md @@ -136,15 +136,16 @@ npm run build 5. Connect Homey Energy Dongle to a Smart Meter 6. Connect the USB-C port of Homey Energy Dongle to your PC 7. Run the example script: + - Replace `` with either `dsmr` or `dlms`. ```sh -node examples/homey-energy-dongle-usb.js +node examples/homey-energy-dongle-usb.js ``` If the data from your meter is encrypted, you'll need to provide the decryption key and the specific serial port to connect to. For example: ```sh -node examples/homey-energy-dongle-usb.js /dev/tty.usbmodem101 1234567890123456 +node examples/homey-energy-dongle-usb.js dsmr /dev/tty.usbmodem101 1234567890123456 ``` ### Connection Homey Energy Dongle using WebSocket @@ -173,7 +174,8 @@ npm run build 7. Enable the Local API in Homey Energy Dongle's settings in Homey - You can also find Homey Energy Dongle's IP address here 8. Run the example script: + - `mode` must be either `dsmr` or `dlms`. ```sh -node examples/homey-energy-dongle-ws.js +node examples/homey-energy-dongle-ws.js ``` diff --git a/examples/homey-energy-dongle-usb.js b/examples/homey-energy-dongle-usb.js index fff1f75..c92ae38 100644 --- a/examples/homey-energy-dongle-usb.js +++ b/examples/homey-energy-dongle-usb.js @@ -10,10 +10,19 @@ * The script will automatically detect the Homey Energy Dongle and start parsing data from from your Smart Meter! */ import { SerialPort } from 'serialport'; -import { DSMRError, DSMR } from '@athombv/dsmr-parser'; +import { DlmsStreamParser, UnencryptedDSMRStreamParser, EncryptedDSMRStreamParser } from '@athombv/dsmr-parser'; -let serialPortPath = process.argv[2]; -const DECRYPTION_KEY = process.argv[3]; +const MODE = process.argv[2]; +let serialPortPath = process.argv[3]; +const DECRYPTION_KEY = process.argv[4]; + +if (!MODE || (MODE !== 'dsmr' && MODE !== 'dlms')) { + console.log( + 'Usage: node examples/homey-energy-dongle-usb.js ', + ); + console.log('No valid mode provided. Use "dsmr" or "dlms".'); + process.exit(1); +} if (!serialPortPath) { const allPorts = await SerialPort.list(); @@ -25,7 +34,7 @@ if (!serialPortPath) { if (possiblePorts.length === 0) { console.log( - 'Usage: node examples/homey-energy-dongle-usb.js ', + 'Usage: node examples/homey-energy-dongle-usb.js ', ); console.log('No Homey Energy Dongle found.'); process.exit(1); @@ -35,7 +44,7 @@ if (!serialPortPath) { console.log(`- ${port.path}`); } console.log( - 'Usage: node examples/homey-energy-dongle-usb.js ', + 'Usage: node examples/homey-energy-dongle-usb.js ', ); process.exit(1); } else { @@ -66,25 +75,51 @@ const serialPort = new SerialPort( }, ); -const parser = DSMR.createStreamParser({ - stream: serialPort, - decryptionKey: DECRYPTION_KEY, - detectEncryption: true, - callback: (error, result) => { - if (error instanceof DSMRError) { - console.error('Error parsing DSMR data:', error.message); - console.error('Raw data:', error.rawTelegram?.toString('hex')); - } else if (error) { - console.error('Error:', error); - } else { - console.log('Raw telegram:'); - console.log(result.raw); - console.log('Parsed telegram:'); - delete result.raw; - console.dir(result, { depth: Infinity }); - } - }, -}); +// Create a DSMR parser that listens to the UART stream. +const parser = (() => { + if (MODE === 'dsmr' && !DECRYPTION_KEY) { + return new UnencryptedDSMRStreamParser({ + stream, + detectEncryption: true, + callback: (error, result) => { + if (error) { + console.error('Error parsing DSMR data:', error); + } else { + console.log('Parsed telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }); + } + + if (MODE === 'dsmr' && DECRYPTION_KEY) { + return new EncryptedDSMRStreamParser({ + stream, + decryptionKey: DECRYPTION_KEY, + callback: (error, result) => { + if (error) { + console.error('Error parsing DSMR data:', error); + } else { + console.log('Parsed telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }) + } + + return new DlmsStreamParser({ + stream, + decryptionKey: DECRYPTION_KEY, + callback: (error, result) => { + if (error) { + console.log('Error parsing DLMS data:', error.message); + } else { + console.log('Parsed DLMS telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }); +})(); // Make sure to close the port when the process exits process.on('SIGINT', () => { diff --git a/examples/homey-energy-dongle-ws.js b/examples/homey-energy-dongle-ws.js index 724f046..e7f2a1c 100644 --- a/examples/homey-energy-dongle-ws.js +++ b/examples/homey-energy-dongle-ws.js @@ -14,17 +14,24 @@ */ import WebSocket, { createWebSocketStream } from 'ws'; -import { DSMRError, DSMR } from '@athombv/dsmr-parser'; +import { DlmsStreamParser, UnencryptedDSMRStreamParser, EncryptedDSMRStreamParser } from '@athombv/dsmr-parser'; const ENERGY_DONGLE_IP = process.argv[2]; -const DECRYPTION_KEY = process.argv[3]; +const MODE = process.argv[3]; +const DECRYPTION_KEY = process.argv[4]; if (!ENERGY_DONGLE_IP) { - console.log('Usage: node examples/homey-energy-dongle-ws.js '); + console.log('Usage: node examples/homey-energy-dongle-ws.js '); console.log('No IP address provided.'); process.exit(1); } +if (!MODE || (MODE !== 'dsmr' && MODE !== 'dlms')) { + console.log('Usage: node examples/homey-energy-dongle-ws.js '); + console.log('No valid mode provided. Use "dsmr" or "dlms".'); + process.exit(1); +} + if (DECRYPTION_KEY) { console.log(`Decryption key: ${DECRYPTION_KEY}`); } @@ -105,24 +112,68 @@ while (true) { }); // Create a DSMR parser that listens to the stream. - const parser = DSMR.createStreamParser({ - stream, - decryptionKey: DECRYPTION_KEY, - detectEncryption: true, - callback: (error, result) => { - if (error instanceof DSMRError) { - console.error('Error parsing DSMR data:', error.message); - console.error('Raw data:', error.rawTelegram?.toString('hex')); - } else if (error) { - console.error('Error:', error); - } else { - // Not very useful to log the raw telegram here as it is already logged by the data listener on the stream. - delete result.raw; - console.log('Parsed telegram:'); - console.dir(result, { depth: Infinity }); - } - }, - }); + const parser = (() => { + if (MODE === 'dsmr' && !DECRYPTION_KEY) { + return new UnencryptedDSMRStreamParser({ + stream, + detectEncryption: true, + callback: (error, result) => { + if (error) { + console.error('Error parsing DSMR data:', error); + } else { + console.log('Parsed telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }); + } + + if (MODE === 'dsmr' && DECRYPTION_KEY) { + return new EncryptedDSMRStreamParser({ + stream, + decryptionKey: DECRYPTION_KEY, + callback: (error, result) => { + if (error) { + console.error('Error parsing DSMR data:', error); + } else { + console.log('Parsed telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }) + } + + return new DlmsStreamParser({ + stream, + decryptionKey: DECRYPTION_KEY, + callback: (error, result) => { + if (error) { + console.log('Error parsing DLMS data:', error.message); + } else { + console.log('Parsed DLMS telegram:'); + console.dir(result, { depth: Infinity }); + } + }, + }); + })(); + // const parser = DSMR.createStreamParser({ + // stream, + // decryptionKey: DECRYPTION_KEY, + // detectEncryption: true, + // callback: (error, result) => { + // if (error instanceof DSMRError) { + // console.error('Error parsing DSMR data:', error.message); + // console.error('Raw data:', error.rawTelegram?.toString('hex')); + // } else if (error) { + // console.error('Error:', error); + // } else { + // // Not very useful to log the raw telegram here as it is already logged by the data listener on the stream. + // delete result.raw; + // console.log('Parsed telegram:'); + // console.dir(result, { depth: Infinity }); + // } + // }, + // }); // Don't continue the loop until the connection is closed. await new Promise((resolve) => { diff --git a/src/index.ts b/src/index.ts index cd9f7e7..b2856af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,3 +16,4 @@ export const DSMR = { export { EncryptedDSMRStreamParser } from './stream/stream-encrypted-dsmr.js'; export { UnencryptedDSMRStreamParser } from './stream/stream-unencrypted-dsmr.js'; export { DlmsStreamParser } from './stream/stream-dlms.js'; +export { SmartMeterDetectTypeStream } from './stream/stream-detect-type.js'; diff --git a/src/stream/stream-detect-type.ts b/src/stream/stream-detect-type.ts index 4534404..4575737 100644 --- a/src/stream/stream-detect-type.ts +++ b/src/stream/stream-detect-type.ts @@ -24,10 +24,10 @@ type StreamDetectTypeCallback = (result: { }) => void; /** This class detects the type of stream (DSMR or DLMS) and whether it is encrypted or not. */ -export class StreamDetectType implements SmartMeterStreamParser { +export class SmartMeterDetectTypeStream implements SmartMeterStreamParser { public readonly startOfFrameByte = DSMR_SOF; - private boundOnData: StreamDetectType['onData']; + private boundOnData: SmartMeterDetectTypeStream['onData']; private telegram = Buffer.alloc(0); constructor( diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index b91b308..cfd85e7 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -29,6 +29,10 @@ export type DlmsStreamParserOptions = { * valid start of frame/header is received. */ fullFrameRequiredWithinMs?: number; + /** + * Data that is already available in the stream when the parser is created. + */ + initialData?: Buffer; }; export class DlmsStreamParser implements SmartMeterStreamParser { @@ -51,6 +55,10 @@ export class DlmsStreamParser implements SmartMeterStreamParser { this.options.stream.addListener('data', this.boundOnData); this.fullFrameRequiredWithinMs = options.fullFrameRequiredWithinMs ?? 5000; + + if (this.options.initialData) { + this.onData(this.options.initialData); + } } private onData(data: Buffer) { diff --git a/src/stream/stream-encrypted-dsmr.ts b/src/stream/stream-encrypted-dsmr.ts index 7fb5dfa..214e454 100644 --- a/src/stream/stream-encrypted-dsmr.ts +++ b/src/stream/stream-encrypted-dsmr.ts @@ -26,6 +26,10 @@ export type DSMRStreamParserOptions = Omit & { * valid start of frame/header is received. */ fullFrameRequiredWithinMs?: number; + /** + * Data that is already available in the stream when the parser is created. + */ + initialData?: Buffer; }; export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { @@ -45,6 +49,10 @@ export class EncryptedDSMRStreamParser implements SmartMeterStreamParser { this.options.stream.addListener('data', this.boundOnData); this.fullFrameRequiredWithinMs = options.fullFrameRequiredWithinMs ?? 5000; + + if (this.options.initialData) { + this.onData(this.options.initialData); + } } private onData(data: Buffer) { diff --git a/src/stream/stream-unencrypted-dsmr.ts b/src/stream/stream-unencrypted-dsmr.ts index d84b979..80d5eb6 100644 --- a/src/stream/stream-unencrypted-dsmr.ts +++ b/src/stream/stream-unencrypted-dsmr.ts @@ -31,6 +31,10 @@ export class UnencryptedDSMRStreamParser implements SmartMeterStreamParser { // End of frame is \r\n!\r\n with the CRC being optional as // it is only for DSMR 4 and up. this.eofRegex = /\r\n!([0-9A-Fa-f]{4})?\r\n(\0)?/; + + if (this.options.initialData) { + this.onData(this.options.initialData); + } } private onData(dataRaw: Buffer) { diff --git a/tests/stream/stream-detect-type.spec.ts b/tests/stream/stream-detect-type.spec.ts index 7c0eb9d..80c8537 100644 --- a/tests/stream/stream-detect-type.spec.ts +++ b/tests/stream/stream-detect-type.spec.ts @@ -2,7 +2,7 @@ import assert from 'node:assert'; import { PassThrough } from 'node:stream'; import { describe, it, mock } from 'node:test'; import { chunkBuffer, readHexFile, readDsmrTelegramFromFiles } from './../test-utils.js'; -import { StreamDetectType } from '../../src/stream/stream-detect-type.js'; +import { SmartMeterDetectTypeStream } from '../../src/stream/stream-detect-type.js'; describe('Stream: Detect Type', () => { it('Detects unencrypted DSMR telegrams', async () => { @@ -12,7 +12,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); @@ -34,7 +34,7 @@ describe('Stream: Detect Type', () => { const callback = mock.fn(); const chunks = chunkBuffer(Buffer.from(input), 1); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); for (const chunk of chunks) { stream.write(chunk); @@ -62,7 +62,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); @@ -84,7 +84,7 @@ describe('Stream: Detect Type', () => { const callback = mock.fn(); const chunks = chunkBuffer(input, 1); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); for (const chunk of chunks) { stream.write(chunk); @@ -110,7 +110,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); @@ -130,7 +130,7 @@ describe('Stream: Detect Type', () => { const callback = mock.fn(); const chunks = chunkBuffer(input, 1); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); for (const chunk of chunks) { stream.write(chunk); @@ -158,7 +158,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); @@ -180,7 +180,7 @@ describe('Stream: Detect Type', () => { const callback = mock.fn(); const chunks = chunkBuffer(input, 1); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); for (const chunk of chunks) { stream.write(chunk); @@ -206,7 +206,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); @@ -223,7 +223,7 @@ describe('Stream: Detect Type', () => { const stream = new PassThrough(); const callback = mock.fn(); - const detector = new StreamDetectType({ stream, callback }); + const detector = new SmartMeterDetectTypeStream({ stream, callback }); stream.write(input); stream.end(); diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt index e5af3b2..b51ba77 100644 --- a/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-with-aad.txt @@ -1,39 +1,39 @@ 7e a2 60 03 05 00 03 3b e6 e7 00 db 08 73 79 73 -74 69 74 6c 65 82 02 47 30 11 22 33 44 95 e9 9b -02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c ef 2b -7d 8e 51 e4 27 37 9e e2 5c a3 14 fd 89 bf 9c c0 -5a ef 0c 03 4b 27 d1 17 d3 79 b9 0f 25 97 10 68 -68 42 82 e8 b4 ab 8e 63 a4 0f d1 43 a6 99 46 e1 -7b 61 53 41 72 52 2c 39 96 48 11 85 50 85 cb 8a -f7 8b b3 7a a1 82 1f f8 f9 34 77 d5 b0 f0 b5 4b -d0 2c 38 30 f8 35 7a 9e c3 42 0b 95 a7 16 9f 81 -6b f0 89 15 d6 ef d1 fa 8d a7 27 b1 25 fc 0c 48 -ee a2 f7 81 26 b6 ab 40 cc f1 c6 98 5d db cc 59 -ac a4 7f 52 76 64 79 96 4e 0b 86 bb da fc 1e 3a -a8 65 77 cf d7 b9 75 be 1e d0 a7 c9 41 6b f1 6a -71 18 2c be 1a 7f 84 9e e7 00 ba cd 56 b1 d7 af -81 fe 7e 7d e2 3e c8 b6 ab 2d 09 d7 95 e7 43 90 -c6 03 5e ba 3e 6d 09 30 cf 0e 0c cd 59 16 ab 72 -6b 69 e5 31 6b 92 c0 8c f4 00 ca 45 6d d2 5d 41 -f0 8a a4 9a 5a ca fd c2 af 1b 9c df 08 01 d1 21 -5b b7 f0 51 97 07 46 b2 b0 f9 df a6 36 c9 36 a4 -b8 f8 59 07 9c e7 fe b6 90 bb 12 4f 0b 7a 1c 28 -c1 69 86 9e 07 5e 8e 23 0c ba b2 4f 65 5f 39 4e -fe 6f 80 d7 6f 22 50 37 05 16 fa 21 e9 14 b6 c6 -c6 4e 25 87 f5 93 5a 48 b9 37 a3 fe 24 2e f1 ea -0a 9f e4 08 ee d7 41 4b 2a c8 6a e9 bf 36 42 89 -d4 03 92 39 2d 57 a9 8b 2d 5d eb 37 4f 93 4c 83 -e8 35 d7 dd a8 d3 01 f8 71 bc 8e 5c 35 0a 1a ab -fc 22 cd 10 0d 2f 97 e8 6c 3d 62 c1 76 19 2e 05 -22 70 75 ad 04 b7 17 04 a2 42 60 25 21 f9 21 78 -36 bf f5 b5 47 b9 c1 cd 13 67 db de 97 1e 78 63 -b7 ed 6a 0b fc f5 55 41 86 da f9 f2 9f b5 34 a7 -a0 05 95 fa 62 9c fc 63 57 24 96 b8 30 de 11 44 -87 57 0d 0a 50 73 3d 4c 0e c8 26 75 38 da 2f 6e -0b 0c 4a 59 34 92 26 e9 14 74 ed 95 96 d1 39 f2 -fa 10 b8 a7 1f 51 45 e0 71 74 ea e6 ee b0 49 21 -a2 bd d5 ee a0 75 bc 69 13 b8 21 de ae 82 37 bb -50 6e 21 e2 c1 b0 ae 8c 8f 33 18 2e f5 01 8e 17 -44 12 59 a9 34 c6 54 24 69 ff 89 da 3f ee 59 58 -a1 4c 62 12 bf 22 f3 ff 5c 29 df bb fe da f9 6d -eb 7e +74 69 74 6c 65 82 02 47 30 11 22 33 44 ae de 35 +2b 8d f1 d2 c9 1a b7 1a 12 86 73 dd 23 55 21 bb +a7 e9 fc 81 f7 14 55 26 51 29 46 40 b8 94 f7 30 +d6 1f b6 b2 a1 86 6c 82 c0 ea 39 ad 68 bc 1f 9a +41 56 fd f0 b3 43 b8 fb ff 14 26 e4 8b 2d a6 93 +97 f3 51 81 3c f9 d3 8c 7d 12 75 67 1d 34 a2 53 +b3 ce 09 66 5a c8 3c 4a 82 f6 5e bf d9 90 fc c3 +cd 90 2b 0c b1 00 4f f6 41 6a 0d 89 b6 a7 4f 94 +c4 5c 26 c8 eb 12 78 ca a9 7c 17 33 0a 80 16 20 +ed c2 2c ca 48 b6 2a f0 b8 32 f4 c5 25 55 c5 5a +6a 08 c9 87 ce 2d 38 40 9e 9e 8a 1a fd 51 e0 f8 +8a 62 a8 bf 14 69 3e 44 7d b4 56 1a 85 2b 6a 6c +b8 b6 78 33 80 6a 94 9f 7f f7 bd f2 a9 a6 86 88 +14 ba 56 56 44 19 c1 f2 b9 67 fc 90 fa f5 80 cf +ee e1 75 4e c7 fb 3a c9 f1 a9 69 a3 cb a6 d3 a3 +26 27 9f e5 da 64 74 c7 d6 3b 0a db 1e 58 2c d3 +38 8d be 26 4e 70 37 96 1b a8 10 ec 0a f3 98 e6 +4d d3 97 5b c0 a4 cf 89 a4 5d d3 9a 86 c2 88 1a +cb 59 ac 91 08 2c 19 53 f1 ca 7f c7 1d e5 d5 76 +2a da 32 b1 d5 95 a6 d4 38 c3 bc 14 1e 3f eb 1f +4e 27 7a bd 92 47 1c b5 ad 29 09 72 18 ec bc ca +31 bd 8a 36 fb 80 56 b8 9b d4 f6 52 a1 0c 4e a8 +49 e6 93 99 d7 f3 dd d0 ca ab 44 63 69 22 e7 7f +c1 56 3c 34 b1 43 ed 67 20 a0 5f a9 64 b5 d9 29 +eb 80 29 35 78 c0 85 6b 74 bb 32 07 56 36 84 ca +53 a7 3d f4 c6 8c f4 d4 98 49 00 ae 85 1e ec be +02 f8 af 81 ae 58 d7 f4 12 88 c7 72 73 69 32 b0 +ad 5f 26 86 f7 9f 04 29 b9 d9 a6 6e f4 da e0 e3 +71 ff 02 02 99 7d 72 98 6b 89 82 1c 17 70 c0 0a +2d ae 09 91 f5 66 d4 ee 47 e8 7d 3f 9b 9d a4 0c +5b 35 dd 58 d7 30 6c 57 84 04 4f 70 47 c3 c0 ec +15 54 7f 53 da 76 ac b8 eb 61 42 ef df e5 9c 40 +7b 60 f7 0f b8 e3 2b 46 37 08 8a 0e 99 87 e9 79 +e8 c4 f2 32 20 20 ba f5 6e ea af 88 e2 fb aa 26 +c5 e9 ac 98 3d 3a 33 d9 a4 6f 52 ab 8f 46 d7 a3 +30 8a 0a 75 22 04 46 a0 6a db af 43 6b cc be 9a +aa 0a 22 d2 a2 d0 1f fe 1e dd f4 0c 56 e7 92 7b +7a 7e diff --git a/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt index b4fc684..f0cb750 100644 --- a/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt +++ b/tests/telegrams/dlms/encrypted/aidon-example-2-without-aad.txt @@ -1,39 +1,39 @@ 7e a2 60 03 05 00 03 3b e6 e7 00 db 08 73 79 73 -74 69 74 6c 65 82 02 47 30 11 22 33 44 95 e9 9b -02 ab fc 7c 0f e8 d7 10 fd 03 0b 95 8d 4c ef 2b -7d 8e 51 e4 27 37 9e e2 5c a3 14 fd 89 bf 9c c0 -5a ef 0c 03 4b 27 d1 17 d3 79 b9 0f 25 97 10 68 -68 42 82 e8 b4 ab 8e 63 a4 0f d1 43 a6 99 46 e1 -7b 61 53 41 72 52 2c 39 96 48 11 85 50 85 cb 8a -f7 8b b3 7a a1 82 1f f8 f9 34 77 d5 b0 f0 b5 4b -d0 2c 38 30 f8 35 7a 9e c3 42 0b 95 a7 16 9f 81 -6b f0 89 15 d6 ef d1 fa 8d a7 27 b1 25 fc 0c 48 -ee a2 f7 81 26 b6 ab 40 cc f1 c6 98 5d db cc 59 -ac a4 7f 52 76 64 79 96 4e 0b 86 bb da fc 1e 3a -a8 65 77 cf d7 b9 75 be 1e d0 a7 c9 41 6b f1 6a -71 18 2c be 1a 7f 84 9e e7 00 ba cd 56 b1 d7 af -81 fe 7e 7d e2 3e c8 b6 ab 2d 09 d7 95 e7 43 90 -c6 03 5e ba 3e 6d 09 30 cf 0e 0c cd 59 16 ab 72 -6b 69 e5 31 6b 92 c0 8c f4 00 ca 45 6d d2 5d 41 -f0 8a a4 9a 5a ca fd c2 af 1b 9c df 08 01 d1 21 -5b b7 f0 51 97 07 46 b2 b0 f9 df a6 36 c9 36 a4 -b8 f8 59 07 9c e7 fe b6 90 bb 12 4f 0b 7a 1c 28 -c1 69 86 9e 07 5e 8e 23 0c ba b2 4f 65 5f 39 4e -fe 6f 80 d7 6f 22 50 37 05 16 fa 21 e9 14 b6 c6 -c6 4e 25 87 f5 93 5a 48 b9 37 a3 fe 24 2e f1 ea -0a 9f e4 08 ee d7 41 4b 2a c8 6a e9 bf 36 42 89 -d4 03 92 39 2d 57 a9 8b 2d 5d eb 37 4f 93 4c 83 -e8 35 d7 dd a8 d3 01 f8 71 bc 8e 5c 35 0a 1a ab -fc 22 cd 10 0d 2f 97 e8 6c 3d 62 c1 76 19 2e 05 -22 70 75 ad 04 b7 17 04 a2 42 60 25 21 f9 21 78 -36 bf f5 b5 47 b9 c1 cd 13 67 db de 97 1e 78 63 -b7 ed 6a 0b fc f5 55 41 86 da f9 f2 9f b5 34 a7 -a0 05 95 fa 62 9c fc 63 57 24 96 b8 30 de 11 44 -87 57 0d 0a 50 73 3d 4c 0e c8 26 75 38 da 2f 6e -0b 0c 4a 59 34 92 26 e9 14 74 ed 95 96 d1 39 f2 -fa 10 b8 a7 1f 51 45 e0 71 74 ea e6 ee b0 49 21 -a2 bd d5 ee a0 75 bc 69 13 b8 21 de ae 82 37 bb -50 6e 21 e2 c1 b0 ae 8c 8f 33 18 2e f5 01 8e 17 -44 12 59 a9 34 c6 54 24 69 ff 89 da 3f ee 59 58 -a1 4c 62 53 05 59 1f 1d d7 6d 10 46 5b 88 9f 26 -74 7e +74 69 74 6c 65 82 02 47 30 11 22 33 44 ae de 35 +2b 8d f1 d2 c9 1a b7 1a 12 86 73 dd 23 55 21 bb +a7 e9 fc 81 f7 14 55 26 51 29 46 40 b8 94 f7 30 +d6 1f b6 b2 a1 86 6c 82 c0 ea 39 ad 68 bc 1f 9a +41 56 fd f0 b3 43 b8 fb ff 14 26 e4 8b 2d a6 93 +97 f3 51 81 3c f9 d3 8c 7d 12 75 67 1d 34 a2 53 +b3 ce 09 66 5a c8 3c 4a 82 f6 5e bf d9 90 fc c3 +cd 90 2b 0c b1 00 4f f6 41 6a 0d 89 b6 a7 4f 94 +c4 5c 26 c8 eb 12 78 ca a9 7c 17 33 0a 80 16 20 +ed c2 2c ca 48 b6 2a f0 b8 32 f4 c5 25 55 c5 5a +6a 08 c9 87 ce 2d 38 40 9e 9e 8a 1a fd 51 e0 f8 +8a 62 a8 bf 14 69 3e 44 7d b4 56 1a 85 2b 6a 6c +b8 b6 78 33 80 6a 94 9f 7f f7 bd f2 a9 a6 86 88 +14 ba 56 56 44 19 c1 f2 b9 67 fc 90 fa f5 80 cf +ee e1 75 4e c7 fb 3a c9 f1 a9 69 a3 cb a6 d3 a3 +26 27 9f e5 da 64 74 c7 d6 3b 0a db 1e 58 2c d3 +38 8d be 26 4e 70 37 96 1b a8 10 ec 0a f3 98 e6 +4d d3 97 5b c0 a4 cf 89 a4 5d d3 9a 86 c2 88 1a +cb 59 ac 91 08 2c 19 53 f1 ca 7f c7 1d e5 d5 76 +2a da 32 b1 d5 95 a6 d4 38 c3 bc 14 1e 3f eb 1f +4e 27 7a bd 92 47 1c b5 ad 29 09 72 18 ec bc ca +31 bd 8a 36 fb 80 56 b8 9b d4 f6 52 a1 0c 4e a8 +49 e6 93 99 d7 f3 dd d0 ca ab 44 63 69 22 e7 7f +c1 56 3c 34 b1 43 ed 67 20 a0 5f a9 64 b5 d9 29 +eb 80 29 35 78 c0 85 6b 74 bb 32 07 56 36 84 ca +53 a7 3d f4 c6 8c f4 d4 98 49 00 ae 85 1e ec be +02 f8 af 81 ae 58 d7 f4 12 88 c7 72 73 69 32 b0 +ad 5f 26 86 f7 9f 04 29 b9 d9 a6 6e f4 da e0 e3 +71 ff 02 02 99 7d 72 98 6b 89 82 1c 17 70 c0 0a +2d ae 09 91 f5 66 d4 ee 47 e8 7d 3f 9b 9d a4 0c +5b 35 dd 58 d7 30 6c 57 84 04 4f 70 47 c3 c0 ec +15 54 7f 53 da 76 ac b8 eb 61 42 ef df e5 9c 40 +7b 60 f7 0f b8 e3 2b 46 37 08 8a 0e 99 87 e9 79 +e8 c4 f2 32 20 20 ba f5 6e ea af 88 e2 fb aa 26 +c5 e9 ac 98 3d 3a 33 d9 a4 6f 52 ab 8f 46 d7 a3 +30 8a 0a 75 22 04 46 a0 6a db af 43 6b cc be 9a +aa 0a 22 65 e9 6c bc c3 da ac 03 02 63 3b 35 0d +26 7e diff --git a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt index 97e7c95..c59130c 100644 --- a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt +++ b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-with-aad.txt @@ -1,95 +1,95 @@ db 08 73 79 73 74 69 74 6c 65 82 05 de 30 11 22 -33 44 b5 e5 ee 7a 9e a0 4f 21 d9 e3 20 cf 37 3c -a5 d2 01 1d 28 40 a4 81 d2 07 18 ab f7 4c 74 a0 -cf 5f b0 95 f9 71 de 36 33 62 17 00 21 fb 4b 8d -5d 10 a5 29 59 4e 6d b1 de 8a fe a6 6e ac 38 fc -8c 9a ad 74 cf 4b 4d 61 66 47 77 03 0a a1 76 24 -b5 63 b6 ff b9 3b bd 80 4d 97 58 2e ca c5 04 52 -f8 81 c1 8f 78 e2 1b 0f 03 d1 c7 76 af ee 72 31 -a6 8b 21 b1 a7 5e c2 ba 2c e0 dc e4 cb bc 92 eb -8b 4e ab 66 63 ec 57 d0 8d 14 8f 90 68 f5 df c5 -b7 6d 14 ec 69 d7 96 53 6d bd 42 72 ff 1a 6a a9 -b7 d0 8a 34 0a 6d 46 59 f7 fb 8b 52 71 38 c1 95 -fa 78 43 c8 59 64 35 47 37 69 04 ef b5 e8 05 74 -f6 45 89 e0 88 bf d1 4e 61 d5 0e 07 94 94 dd 25 -e2 a2 29 7f d8 b2 61 25 d4 16 60 4b 06 e2 c1 24 -f5 73 23 87 4d bc 4f f6 1d 58 ab f3 a7 9f 42 e4 -48 98 e5 70 71 ca b8 88 af 7b fa c3 e9 9d 36 a5 -e9 39 2b ac 71 72 45 fc 60 ba 37 7c 83 9c c1 f1 -80 05 fb 05 83 8e c9 69 3a f0 91 60 c2 b9 b6 18 -7e 24 48 29 1c f9 43 aa ad 26 68 bf 0d 24 8d 82 -9a 08 29 58 3c d7 60 88 e8 42 04 77 04 31 31 cc -0e d9 15 81 f0 00 66 15 ad 9a a7 19 63 bb 3d 84 -c8 16 17 c1 c2 3c b1 fe 27 de 18 69 7b 18 f9 40 -80 eb 78 6b 92 c5 30 bc 00 11 67 99 8e 1d 73 24 -19 7f a3 67 51 da 07 f2 b6 e8 8f 2a f6 72 8b a2 -6d 23 3e 2b 7a ce 0c fd 38 3c 1d a5 cd 2d 02 57 -ee 45 26 1b 2e 63 64 7f a7 cb 9c 27 3e 9b 74 4c -14 00 c8 07 4b 05 95 f1 b9 76 94 ce f0 2a ae f3 -ed b9 2f 48 49 85 cb 67 17 d7 da 64 72 b9 ed d7 -fe b6 84 fb 89 90 2c 98 f0 50 b3 c3 59 78 0f ba -8c 17 ea 21 6c f7 60 3d cc 6f 5a 30 46 3f e7 14 -40 0b fe 1c 5f 3a 2b 7c 70 04 a3 1e dd db 5b e0 -06 fe 7a 0b ca c0 22 80 8a 2f 7c 7c ce 40 44 d8 -de dd 66 42 2b 93 90 ed d6 95 48 92 4c 3f 94 12 -ff 98 b3 07 88 6a 47 d3 ee f0 f9 73 fd be 03 39 -1d d5 0f bc 3c 7d 24 68 99 00 e7 59 d1 5e d2 b9 -e0 0f de 75 64 97 74 72 4b d4 e5 3d fc 78 6b f8 -9e df 3e 92 7f e0 8c 0d 4c 30 e9 83 b6 5a 4f cd -4a c6 f9 a9 43 ab 1e fd c9 b5 b9 ce 77 c3 a7 bb -ce 80 a0 7a fc d6 78 cc 2a d8 6b 81 11 e4 5a 82 -27 7e f7 d9 8c 04 db 95 35 b3 5d 4a 6a 1b f0 84 -48 03 b9 e1 6a 79 fd 25 89 a2 3a 74 90 46 9a 25 -dc 5d 68 f4 f3 0b c5 24 e9 c9 c5 b2 7a c7 5d c9 -83 53 34 43 88 eb de 88 71 72 f9 f8 d8 ee 22 30 -43 17 f3 33 de 12 d2 00 41 6f 47 95 f2 60 56 91 -bd 94 f9 49 a3 ae 15 2c 77 7c dc c5 4f 63 e8 68 -31 72 4c 95 97 95 19 8f cb 6a 91 d3 6a 0b 4f 8a -b1 23 49 7b 78 6e 54 3b 96 38 8c e5 27 72 a1 9e -b2 5d 6f 33 a0 97 34 43 8e 57 04 1c 2d e2 8f 89 -dd 13 30 c6 cf 39 9a 4b 8b 25 3a 42 a5 1c 1c 25 -86 89 ab 5d 62 0e 81 a1 bb 78 0a ce 95 6a 95 2a -03 b1 35 98 56 b9 8d 0c 74 e3 2e 5b d3 f9 ae aa -e8 8a 3b 4b 04 39 05 78 a8 22 aa 6a 93 52 f4 54 -b1 91 ef 27 95 dc df b7 cd a4 85 65 63 e3 d0 63 -70 e4 73 80 32 9d 08 10 f8 be 56 72 f0 08 57 34 -fa c0 c1 5c d2 3d b1 24 81 3d f6 53 a0 2e 4d e5 -5a 41 c2 0c d5 19 64 7a cb af 5e f6 1d ca 91 ec -a3 56 0c 17 1c b5 70 8e 0e 5c 04 be c5 1b b3 45 -4e 3c 2c 66 7b 47 3f 51 00 5d fb 73 07 0d 71 d8 -2f 8f cf 0f b7 71 2c 84 23 57 db 2f 3e 4e 9b 24 -9f fe 5a e3 b1 c4 18 11 02 89 63 40 2c 1b d5 be -31 f9 04 88 a7 ba be bb 96 f1 9f 1c a0 79 d5 b0 -10 26 56 03 c9 3d 17 d4 0d 8f 14 c5 2d f0 e3 22 -20 35 96 f8 97 f2 5f 0a 3b e9 34 23 bc c7 d9 7f -93 c1 e3 f0 cc c0 97 68 78 bd 22 df 42 f1 a4 9c -64 96 2b 54 2a eb 93 bb 1d 45 2f 37 ec 72 2f 34 -65 fd 65 40 4b d9 03 c4 b9 f6 3f 68 a0 23 e4 57 -0f f2 6f b0 4b fb 17 57 a5 05 0d 5c 21 f4 9c 44 -04 e5 71 6e 1d c2 7b 9e 33 73 01 f7 45 d9 6a e3 -39 bd 5d 3b 78 5e 64 00 e8 cb 82 0d ad a8 49 99 -fb 95 b0 58 0f a4 fa c6 d1 1f 8f 45 ef cb f3 27 -03 e8 d8 4e ae 6f aa f9 39 e4 75 05 ca d2 04 ef -be c5 00 8d a0 15 49 b6 2c 46 33 6e 3a cf 4d 12 -ef d9 c3 ee 56 e1 51 1b 12 20 23 c8 46 c2 fa 83 -d5 8d f1 67 a6 19 1a 12 1b b5 5c 08 e4 4b f0 e9 -a1 48 d1 f4 e3 29 18 30 59 c0 d7 6b 3d 04 86 26 -18 de 9a e6 66 c3 d2 46 8c 0c a5 7a 99 85 7f 56 -e0 36 4e 5a cf da e1 02 55 d9 bb 90 66 9d 50 df -ba 74 12 3b 44 99 6f fe 31 5e 2f 3f 0d 09 e0 91 -ab 24 cf 04 ac 8e f8 5f a3 bf 32 78 e5 a8 9e c0 -57 db d7 3c 31 1e 7c 2e 4b 26 2e 03 d8 06 7b 14 -34 3a 48 eb d0 cd b2 4c b4 c1 68 be 0d cd 5e 2f -47 17 7a bf 3b ee 54 bf 44 ae a9 a1 28 51 c3 c9 -39 2c b5 ad c2 ca 46 ef 59 77 cc 5a df bb 08 7e -e0 ae b0 33 28 b6 f5 ba 41 2d dd 6d 26 96 c7 5d -5e e4 af 72 ce 7d 91 4c 30 dc 59 c9 a9 58 4f 62 -c3 b0 27 74 e4 1f a7 91 48 4c 19 ac 02 b8 9d c7 -37 79 fd 5d 97 9e 73 4a 3f 8f c4 6c 2e 32 8a 7f -d9 62 d5 f0 14 ff 25 66 78 2f 36 ad 62 88 7d 54 -9f c7 36 f9 40 57 d3 8b ca b4 b4 7e 71 e2 5e 56 -fd 94 3a 9f a5 2b 14 62 05 7b e0 e0 3f 5c 60 ac -95 46 6b 74 40 e2 ac 69 10 3f c7 c1 54 d7 c9 d3 -ce 8a 61 77 e9 95 7a ba 90 2b 7a 40 62 3d c2 4e -78 a7 d8 60 87 c0 8c c0 92 48 e0 ca 4c 13 65 7e -e2 01 24 69 7c 1d 73 77 f0 92 d9 +33 44 8e d2 40 53 b8 ad e1 e7 2b 83 2a 20 b2 44 +ed 7c 18 d3 b8 9a c3 2c b7 d7 3b 60 33 41 fe f2 +72 6e 9b fe 09 fd 2e 8c 82 88 b6 bd b4 e8 d8 0d +ff 5d 8e 26 ab 67 79 ce c6 8d 16 90 f6 f7 23 0b +2b b7 19 94 bd a7 df 63 a6 09 dc fc bf 4a 2c 40 +57 2e 07 96 60 7f f8 3a 51 6c 12 0d 78 be c6 7b +92 e8 a1 c6 f0 ff a7 1c 3f 98 f2 43 c7 6c 5a 37 +ba 9a 90 61 b2 f1 6e 15 f1 dd 21 4d fb 98 49 db +09 61 d7 7c 0b ef 37 0b c6 7a 8f 11 d8 81 1c f7 +ea 15 9a e5 6a 11 3a e5 b8 05 0b 33 29 ca ff a5 +16 f7 27 ca c8 4f 41 86 87 38 5b 19 8b 5b a5 64 +29 bc 03 53 5f ad 9b 13 ba f3 11 ff b4 70 f2 73 +c9 ba 9e b1 af 2a 95 66 4a 73 29 0e d0 86 97 d0 +a5 cd 3b bc 87 9a 83 0e 20 ef f6 78 ff dc 66 41 +9b e1 93 ff 9c f1 01 8c c9 e9 5d 47 ec bd 79 24 +d6 eb 6f 01 e3 02 bf 92 13 6f 40 09 bd 29 85 29 +da 3b d9 e5 b6 64 21 9b 6a ed 94 f5 b8 88 65 fd +bc b5 f0 bb 3d fd 68 9c ac 64 5a 87 27 d8 c7 75 +f6 32 d7 e0 42 12 f0 1e 82 f4 a3 97 fa 10 f4 8c +c1 73 49 8a 6d 67 28 72 82 bf 61 3b 86 99 0e 3f +5d 28 ed 8b fc f7 95 ba 1c 94 b4 15 93 99 de d1 +64 93 35 7e 80 7f c8 89 b6 e7 3c f5 e0 f8 9a 6e +0a 3d 6c ce 64 d0 65 12 0d 8d 73 dd 62 10 8e 90 +87 54 85 f2 fb d9 b2 0c 5e 38 9c ae 65 77 8c 1e +36 40 02 b5 1b 61 89 0d dc f7 be c6 f1 d9 76 35 +81 b6 21 d9 95 43 ec a5 8b 61 73 e7 ce 2b be eb +43 52 58 14 83 9e 75 22 8a c6 b2 0b 14 80 10 8e +5d da eb d0 c9 43 d9 0f 1e b2 52 43 ab 54 be ac +10 3e 41 0f 24 1d 87 04 9b c7 49 eb d4 68 c3 51 +0b bc a9 94 24 2b 02 ed 9e e8 19 61 5d b5 2b 7d +45 74 e7 f3 dd 24 73 49 7a ea 47 94 8c 24 4e 4f +7c b7 4e ae 78 41 52 cf 22 88 ce 12 68 06 38 b8 +36 aa 51 e2 73 d9 e9 ca 0a 15 1d 94 d0 42 c6 9c +a9 d4 ca 9a 15 ff c0 5e 94 0c 73 ee a8 95 5f 73 +98 af 48 e5 88 09 bc 3b 45 16 25 4b 55 5d f6 9f +79 5b fc 92 a6 9c 32 32 0b 85 17 bc ca f0 b1 93 +85 f7 50 e2 93 7c 7b dd 40 9e d6 a6 d8 c7 81 b9 +61 06 5a d7 4e a2 cc 11 bc d1 d5 e7 ea 7f 81 20 +c7 10 af 19 80 92 34 ad 33 8c 3f 22 48 14 b5 e2 +43 61 d6 19 5e 5c fa 89 9f f9 08 09 e0 86 3d eb +67 da ae 2c c6 e0 3b c3 50 19 d3 f7 38 16 e8 d7 +e4 8f d1 27 72 f1 eb 6e a4 79 9b 58 9f 44 be 4a +f2 7d fd ef b0 fd 25 7c 42 3f 87 c7 05 02 11 9b +f9 09 a9 e5 57 43 fb 94 c0 b0 b9 36 ff ed b4 31 +f1 e5 fc 0f 65 d3 7a 3c ac 84 66 e7 cc af 03 55 +f5 60 6b 04 8d cd 21 fe 42 27 3f 80 31 ca 21 5b +3f f8 ba 9c 50 15 82 fd bf e7 81 df 04 51 13 0f +2b 94 80 36 fb 93 7e 3f d7 4a 08 1d 86 c1 e2 0e +6f e7 2d bf 4e dd 8d 22 ba da 90 04 6b 03 5f ce +59 9e 9c aa 16 f9 14 a8 02 65 e2 3f 3f 92 78 1c +e5 8f 5d b7 24 b9 1f 5a aa 8b e7 1d 27 63 50 08 +f0 88 65 a6 12 9c 04 c2 b8 3c 3e f9 8c 0c f8 b7 +6a df ae 04 63 6a e7 db 1d ce 18 6e fc 3b 07 65 +0d 2c 5c 67 e6 0c 45 24 3e 26 7d 31 b1 43 45 62 +f0 85 2f e1 24 dc f3 49 23 7f 28 3b 4c 13 4d da +50 59 65 cb 1b 2f 89 c6 6b 33 81 6f 49 d5 ca 43 +57 07 29 a4 97 cd f5 06 b2 2d b1 84 43 e3 00 d1 +9d 40 04 eb 85 f9 c1 46 89 61 4b 9e 6d 15 11 90 +98 a5 0d 34 3c 9a 4d 39 3d ce 25 a3 c6 a3 c7 78 +98 b3 c1 fa 77 68 82 9f f7 1c d8 3d c2 e3 cc 8b +38 ac 4e af 41 fa 58 11 c5 db 58 50 52 50 ab 71 +af c1 95 46 a3 38 48 e1 f4 5b 7d 7a 54 b4 6e 62 +38 03 44 d6 0c 92 89 da 37 26 21 fc 0a 6a 4b f7 +62 a5 3d 8d 3a 43 c7 e7 76 93 4e bb a4 6c fe 6d +19 b4 da 09 4c ef ff 28 47 b6 d0 f8 a0 c6 b0 54 +9b 95 01 85 b7 f7 f3 da 72 4d 78 00 14 0f 8c c1 +71 cc 68 75 5f 92 90 fd 5a a2 36 a4 34 92 f1 12 +f4 91 7d fa b4 4a 69 48 69 2d bd f3 4f b2 3f c1 +a0 48 5e b9 15 2f 29 2e 59 f2 70 bb 8f 9a 16 30 +cc b5 e5 59 58 a2 50 f1 87 33 8d 8e 5e 49 55 8a +12 5d 48 ad 90 79 9b 24 a9 48 a7 e3 c0 6e 27 58 +ab 5c f6 91 9b d8 b0 eb fd c1 19 ba 34 48 45 ac +cf 01 74 ac 20 83 d2 80 56 2f 5a eb 6a 08 86 65 +5b bf af 79 86 00 58 6b 3a 0d 90 4f 3a d6 b3 5e +be 81 08 cd 0f a8 0a fa be cc c8 cb 27 cf fe e9 +b2 89 cb 05 99 6e 44 91 aa 84 79 97 5b 7c 08 b5 +25 e1 65 9c 45 2b 1c 0f 87 69 24 a6 b9 b2 c3 e8 +8f 55 a3 25 60 13 66 7c bc ab d0 67 16 99 ea 07 +bd 7a 8a 48 03 33 4b 44 32 64 19 17 8d c8 98 d7 +8e 50 3a 72 78 62 89 63 96 de bd dc f6 65 11 3e +88 9a 6f d4 72 08 28 10 53 2b c2 99 80 bd 61 61 +74 91 31 7b 94 4b 76 7b 7e d2 9a c4 58 4f 95 3c +64 57 7e 08 4b cd a4 40 a9 a7 bb 55 33 b9 45 78 +90 e7 7c b8 78 6c e9 b5 ac 98 7b 65 b7 09 8a 56 +4a 5e 40 5a e6 a3 09 43 e3 43 d3 8f 46 51 d5 3f +14 dd 24 d1 f3 a2 92 4e 13 13 6a 13 bd 4a 08 70 +1b 3d bf 83 48 2f 0a 96 16 90 0f 6c 0a 71 9e c4 +33 3a 45 d1 43 6d bd bb a3 a5 f0 b6 c6 06 9d 25 +c1 e3 7c 66 4a bf f4 4f a3 52 12 19 75 60 26 ba +cd da 25 18 88 56 69 e5 be 75 f8 b3 85 4d 07 4f +d1 37 07 6d a0 79 ad a2 55 42 5e 19 6c 9e bb 9d +dd 7b ea 7b 0c e3 7f 59 f1 b0 0d 5f ff 73 ba d3 +2b 6e ca a4 b8 3e fd ae 8c 51 b8 bb 00 a0 de b6 +31 dd 90 f3 d0 ed 17 c1 65 74 16 diff --git a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt index 0c625b4..4bb8f12 100644 --- a/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt +++ b/tests/telegrams/dsmr/encrypted/dsmr-luxembourgh-spec-example-without-aad.txt @@ -1,95 +1,95 @@ db 08 73 79 73 74 69 74 6c 65 82 05 de 30 11 22 -33 44 b5 e5 ee 7a 9e a0 4f 21 d9 e3 20 cf 37 3c -a5 d2 01 1d 28 40 a4 81 d2 07 18 ab f7 4c 74 a0 -cf 5f b0 95 f9 71 de 36 33 62 17 00 21 fb 4b 8d -5d 10 a5 29 59 4e 6d b1 de 8a fe a6 6e ac 38 fc -8c 9a ad 74 cf 4b 4d 61 66 47 77 03 0a a1 76 24 -b5 63 b6 ff b9 3b bd 80 4d 97 58 2e ca c5 04 52 -f8 81 c1 8f 78 e2 1b 0f 03 d1 c7 76 af ee 72 31 -a6 8b 21 b1 a7 5e c2 ba 2c e0 dc e4 cb bc 92 eb -8b 4e ab 66 63 ec 57 d0 8d 14 8f 90 68 f5 df c5 -b7 6d 14 ec 69 d7 96 53 6d bd 42 72 ff 1a 6a a9 -b7 d0 8a 34 0a 6d 46 59 f7 fb 8b 52 71 38 c1 95 -fa 78 43 c8 59 64 35 47 37 69 04 ef b5 e8 05 74 -f6 45 89 e0 88 bf d1 4e 61 d5 0e 07 94 94 dd 25 -e2 a2 29 7f d8 b2 61 25 d4 16 60 4b 06 e2 c1 24 -f5 73 23 87 4d bc 4f f6 1d 58 ab f3 a7 9f 42 e4 -48 98 e5 70 71 ca b8 88 af 7b fa c3 e9 9d 36 a5 -e9 39 2b ac 71 72 45 fc 60 ba 37 7c 83 9c c1 f1 -80 05 fb 05 83 8e c9 69 3a f0 91 60 c2 b9 b6 18 -7e 24 48 29 1c f9 43 aa ad 26 68 bf 0d 24 8d 82 -9a 08 29 58 3c d7 60 88 e8 42 04 77 04 31 31 cc -0e d9 15 81 f0 00 66 15 ad 9a a7 19 63 bb 3d 84 -c8 16 17 c1 c2 3c b1 fe 27 de 18 69 7b 18 f9 40 -80 eb 78 6b 92 c5 30 bc 00 11 67 99 8e 1d 73 24 -19 7f a3 67 51 da 07 f2 b6 e8 8f 2a f6 72 8b a2 -6d 23 3e 2b 7a ce 0c fd 38 3c 1d a5 cd 2d 02 57 -ee 45 26 1b 2e 63 64 7f a7 cb 9c 27 3e 9b 74 4c -14 00 c8 07 4b 05 95 f1 b9 76 94 ce f0 2a ae f3 -ed b9 2f 48 49 85 cb 67 17 d7 da 64 72 b9 ed d7 -fe b6 84 fb 89 90 2c 98 f0 50 b3 c3 59 78 0f ba -8c 17 ea 21 6c f7 60 3d cc 6f 5a 30 46 3f e7 14 -40 0b fe 1c 5f 3a 2b 7c 70 04 a3 1e dd db 5b e0 -06 fe 7a 0b ca c0 22 80 8a 2f 7c 7c ce 40 44 d8 -de dd 66 42 2b 93 90 ed d6 95 48 92 4c 3f 94 12 -ff 98 b3 07 88 6a 47 d3 ee f0 f9 73 fd be 03 39 -1d d5 0f bc 3c 7d 24 68 99 00 e7 59 d1 5e d2 b9 -e0 0f de 75 64 97 74 72 4b d4 e5 3d fc 78 6b f8 -9e df 3e 92 7f e0 8c 0d 4c 30 e9 83 b6 5a 4f cd -4a c6 f9 a9 43 ab 1e fd c9 b5 b9 ce 77 c3 a7 bb -ce 80 a0 7a fc d6 78 cc 2a d8 6b 81 11 e4 5a 82 -27 7e f7 d9 8c 04 db 95 35 b3 5d 4a 6a 1b f0 84 -48 03 b9 e1 6a 79 fd 25 89 a2 3a 74 90 46 9a 25 -dc 5d 68 f4 f3 0b c5 24 e9 c9 c5 b2 7a c7 5d c9 -83 53 34 43 88 eb de 88 71 72 f9 f8 d8 ee 22 30 -43 17 f3 33 de 12 d2 00 41 6f 47 95 f2 60 56 91 -bd 94 f9 49 a3 ae 15 2c 77 7c dc c5 4f 63 e8 68 -31 72 4c 95 97 95 19 8f cb 6a 91 d3 6a 0b 4f 8a -b1 23 49 7b 78 6e 54 3b 96 38 8c e5 27 72 a1 9e -b2 5d 6f 33 a0 97 34 43 8e 57 04 1c 2d e2 8f 89 -dd 13 30 c6 cf 39 9a 4b 8b 25 3a 42 a5 1c 1c 25 -86 89 ab 5d 62 0e 81 a1 bb 78 0a ce 95 6a 95 2a -03 b1 35 98 56 b9 8d 0c 74 e3 2e 5b d3 f9 ae aa -e8 8a 3b 4b 04 39 05 78 a8 22 aa 6a 93 52 f4 54 -b1 91 ef 27 95 dc df b7 cd a4 85 65 63 e3 d0 63 -70 e4 73 80 32 9d 08 10 f8 be 56 72 f0 08 57 34 -fa c0 c1 5c d2 3d b1 24 81 3d f6 53 a0 2e 4d e5 -5a 41 c2 0c d5 19 64 7a cb af 5e f6 1d ca 91 ec -a3 56 0c 17 1c b5 70 8e 0e 5c 04 be c5 1b b3 45 -4e 3c 2c 66 7b 47 3f 51 00 5d fb 73 07 0d 71 d8 -2f 8f cf 0f b7 71 2c 84 23 57 db 2f 3e 4e 9b 24 -9f fe 5a e3 b1 c4 18 11 02 89 63 40 2c 1b d5 be -31 f9 04 88 a7 ba be bb 96 f1 9f 1c a0 79 d5 b0 -10 26 56 03 c9 3d 17 d4 0d 8f 14 c5 2d f0 e3 22 -20 35 96 f8 97 f2 5f 0a 3b e9 34 23 bc c7 d9 7f -93 c1 e3 f0 cc c0 97 68 78 bd 22 df 42 f1 a4 9c -64 96 2b 54 2a eb 93 bb 1d 45 2f 37 ec 72 2f 34 -65 fd 65 40 4b d9 03 c4 b9 f6 3f 68 a0 23 e4 57 -0f f2 6f b0 4b fb 17 57 a5 05 0d 5c 21 f4 9c 44 -04 e5 71 6e 1d c2 7b 9e 33 73 01 f7 45 d9 6a e3 -39 bd 5d 3b 78 5e 64 00 e8 cb 82 0d ad a8 49 99 -fb 95 b0 58 0f a4 fa c6 d1 1f 8f 45 ef cb f3 27 -03 e8 d8 4e ae 6f aa f9 39 e4 75 05 ca d2 04 ef -be c5 00 8d a0 15 49 b6 2c 46 33 6e 3a cf 4d 12 -ef d9 c3 ee 56 e1 51 1b 12 20 23 c8 46 c2 fa 83 -d5 8d f1 67 a6 19 1a 12 1b b5 5c 08 e4 4b f0 e9 -a1 48 d1 f4 e3 29 18 30 59 c0 d7 6b 3d 04 86 26 -18 de 9a e6 66 c3 d2 46 8c 0c a5 7a 99 85 7f 56 -e0 36 4e 5a cf da e1 02 55 d9 bb 90 66 9d 50 df -ba 74 12 3b 44 99 6f fe 31 5e 2f 3f 0d 09 e0 91 -ab 24 cf 04 ac 8e f8 5f a3 bf 32 78 e5 a8 9e c0 -57 db d7 3c 31 1e 7c 2e 4b 26 2e 03 d8 06 7b 14 -34 3a 48 eb d0 cd b2 4c b4 c1 68 be 0d cd 5e 2f -47 17 7a bf 3b ee 54 bf 44 ae a9 a1 28 51 c3 c9 -39 2c b5 ad c2 ca 46 ef 59 77 cc 5a df bb 08 7e -e0 ae b0 33 28 b6 f5 ba 41 2d dd 6d 26 96 c7 5d -5e e4 af 72 ce 7d 91 4c 30 dc 59 c9 a9 58 4f 62 -c3 b0 27 74 e4 1f a7 91 48 4c 19 ac 02 b8 9d c7 -37 79 fd 5d 97 9e 73 4a 3f 8f c4 6c 2e 32 8a 7f -d9 62 d5 f0 14 ff 25 66 78 2f 36 ad 62 88 7d 54 -9f c7 36 f9 40 57 d3 8b ca b4 b4 7e 71 e2 5e 56 -fd 94 3a 9f a5 2b 14 62 05 7b e0 e0 3f 5c 60 ac -95 46 6b 74 40 e2 ac 69 10 3f c7 c1 54 d7 c9 d3 -ce 8a 61 77 e9 95 7a ba 90 2b 7a 40 62 3d c2 4e -78 a7 d8 60 87 c0 8c c0 92 48 e0 ca 4c 13 65 b5 -b4 4e 22 f5 6b 44 16 8a 93 77 0b +33 44 8e d2 40 53 b8 ad e1 e7 2b 83 2a 20 b2 44 +ed 7c 18 d3 b8 9a c3 2c b7 d7 3b 60 33 41 fe f2 +72 6e 9b fe 09 fd 2e 8c 82 88 b6 bd b4 e8 d8 0d +ff 5d 8e 26 ab 67 79 ce c6 8d 16 90 f6 f7 23 0b +2b b7 19 94 bd a7 df 63 a6 09 dc fc bf 4a 2c 40 +57 2e 07 96 60 7f f8 3a 51 6c 12 0d 78 be c6 7b +92 e8 a1 c6 f0 ff a7 1c 3f 98 f2 43 c7 6c 5a 37 +ba 9a 90 61 b2 f1 6e 15 f1 dd 21 4d fb 98 49 db +09 61 d7 7c 0b ef 37 0b c6 7a 8f 11 d8 81 1c f7 +ea 15 9a e5 6a 11 3a e5 b8 05 0b 33 29 ca ff a5 +16 f7 27 ca c8 4f 41 86 87 38 5b 19 8b 5b a5 64 +29 bc 03 53 5f ad 9b 13 ba f3 11 ff b4 70 f2 73 +c9 ba 9e b1 af 2a 95 66 4a 73 29 0e d0 86 97 d0 +a5 cd 3b bc 87 9a 83 0e 20 ef f6 78 ff dc 66 41 +9b e1 93 ff 9c f1 01 8c c9 e9 5d 47 ec bd 79 24 +d6 eb 6f 01 e3 02 bf 92 13 6f 40 09 bd 29 85 29 +da 3b d9 e5 b6 64 21 9b 6a ed 94 f5 b8 88 65 fd +bc b5 f0 bb 3d fd 68 9c ac 64 5a 87 27 d8 c7 75 +f6 32 d7 e0 42 12 f0 1e 82 f4 a3 97 fa 10 f4 8c +c1 73 49 8a 6d 67 28 72 82 bf 61 3b 86 99 0e 3f +5d 28 ed 8b fc f7 95 ba 1c 94 b4 15 93 99 de d1 +64 93 35 7e 80 7f c8 89 b6 e7 3c f5 e0 f8 9a 6e +0a 3d 6c ce 64 d0 65 12 0d 8d 73 dd 62 10 8e 90 +87 54 85 f2 fb d9 b2 0c 5e 38 9c ae 65 77 8c 1e +36 40 02 b5 1b 61 89 0d dc f7 be c6 f1 d9 76 35 +81 b6 21 d9 95 43 ec a5 8b 61 73 e7 ce 2b be eb +43 52 58 14 83 9e 75 22 8a c6 b2 0b 14 80 10 8e +5d da eb d0 c9 43 d9 0f 1e b2 52 43 ab 54 be ac +10 3e 41 0f 24 1d 87 04 9b c7 49 eb d4 68 c3 51 +0b bc a9 94 24 2b 02 ed 9e e8 19 61 5d b5 2b 7d +45 74 e7 f3 dd 24 73 49 7a ea 47 94 8c 24 4e 4f +7c b7 4e ae 78 41 52 cf 22 88 ce 12 68 06 38 b8 +36 aa 51 e2 73 d9 e9 ca 0a 15 1d 94 d0 42 c6 9c +a9 d4 ca 9a 15 ff c0 5e 94 0c 73 ee a8 95 5f 73 +98 af 48 e5 88 09 bc 3b 45 16 25 4b 55 5d f6 9f +79 5b fc 92 a6 9c 32 32 0b 85 17 bc ca f0 b1 93 +85 f7 50 e2 93 7c 7b dd 40 9e d6 a6 d8 c7 81 b9 +61 06 5a d7 4e a2 cc 11 bc d1 d5 e7 ea 7f 81 20 +c7 10 af 19 80 92 34 ad 33 8c 3f 22 48 14 b5 e2 +43 61 d6 19 5e 5c fa 89 9f f9 08 09 e0 86 3d eb +67 da ae 2c c6 e0 3b c3 50 19 d3 f7 38 16 e8 d7 +e4 8f d1 27 72 f1 eb 6e a4 79 9b 58 9f 44 be 4a +f2 7d fd ef b0 fd 25 7c 42 3f 87 c7 05 02 11 9b +f9 09 a9 e5 57 43 fb 94 c0 b0 b9 36 ff ed b4 31 +f1 e5 fc 0f 65 d3 7a 3c ac 84 66 e7 cc af 03 55 +f5 60 6b 04 8d cd 21 fe 42 27 3f 80 31 ca 21 5b +3f f8 ba 9c 50 15 82 fd bf e7 81 df 04 51 13 0f +2b 94 80 36 fb 93 7e 3f d7 4a 08 1d 86 c1 e2 0e +6f e7 2d bf 4e dd 8d 22 ba da 90 04 6b 03 5f ce +59 9e 9c aa 16 f9 14 a8 02 65 e2 3f 3f 92 78 1c +e5 8f 5d b7 24 b9 1f 5a aa 8b e7 1d 27 63 50 08 +f0 88 65 a6 12 9c 04 c2 b8 3c 3e f9 8c 0c f8 b7 +6a df ae 04 63 6a e7 db 1d ce 18 6e fc 3b 07 65 +0d 2c 5c 67 e6 0c 45 24 3e 26 7d 31 b1 43 45 62 +f0 85 2f e1 24 dc f3 49 23 7f 28 3b 4c 13 4d da +50 59 65 cb 1b 2f 89 c6 6b 33 81 6f 49 d5 ca 43 +57 07 29 a4 97 cd f5 06 b2 2d b1 84 43 e3 00 d1 +9d 40 04 eb 85 f9 c1 46 89 61 4b 9e 6d 15 11 90 +98 a5 0d 34 3c 9a 4d 39 3d ce 25 a3 c6 a3 c7 78 +98 b3 c1 fa 77 68 82 9f f7 1c d8 3d c2 e3 cc 8b +38 ac 4e af 41 fa 58 11 c5 db 58 50 52 50 ab 71 +af c1 95 46 a3 38 48 e1 f4 5b 7d 7a 54 b4 6e 62 +38 03 44 d6 0c 92 89 da 37 26 21 fc 0a 6a 4b f7 +62 a5 3d 8d 3a 43 c7 e7 76 93 4e bb a4 6c fe 6d +19 b4 da 09 4c ef ff 28 47 b6 d0 f8 a0 c6 b0 54 +9b 95 01 85 b7 f7 f3 da 72 4d 78 00 14 0f 8c c1 +71 cc 68 75 5f 92 90 fd 5a a2 36 a4 34 92 f1 12 +f4 91 7d fa b4 4a 69 48 69 2d bd f3 4f b2 3f c1 +a0 48 5e b9 15 2f 29 2e 59 f2 70 bb 8f 9a 16 30 +cc b5 e5 59 58 a2 50 f1 87 33 8d 8e 5e 49 55 8a +12 5d 48 ad 90 79 9b 24 a9 48 a7 e3 c0 6e 27 58 +ab 5c f6 91 9b d8 b0 eb fd c1 19 ba 34 48 45 ac +cf 01 74 ac 20 83 d2 80 56 2f 5a eb 6a 08 86 65 +5b bf af 79 86 00 58 6b 3a 0d 90 4f 3a d6 b3 5e +be 81 08 cd 0f a8 0a fa be cc c8 cb 27 cf fe e9 +b2 89 cb 05 99 6e 44 91 aa 84 79 97 5b 7c 08 b5 +25 e1 65 9c 45 2b 1c 0f 87 69 24 a6 b9 b2 c3 e8 +8f 55 a3 25 60 13 66 7c bc ab d0 67 16 99 ea 07 +bd 7a 8a 48 03 33 4b 44 32 64 19 17 8d c8 98 d7 +8e 50 3a 72 78 62 89 63 96 de bd dc f6 65 11 3e +88 9a 6f d4 72 08 28 10 53 2b c2 99 80 bd 61 61 +74 91 31 7b 94 4b 76 7b 7e d2 9a c4 58 4f 95 3c +64 57 7e 08 4b cd a4 40 a9 a7 bb 55 33 b9 45 78 +90 e7 7c b8 78 6c e9 b5 ac 98 7b 65 b7 09 8a 56 +4a 5e 40 5a e6 a3 09 43 e3 43 d3 8f 46 51 d5 3f +14 dd 24 d1 f3 a2 92 4e 13 13 6a 13 bd 4a 08 70 +1b 3d bf 83 48 2f 0a 96 16 90 0f 6c 0a 71 9e c4 +33 3a 45 d1 43 6d bd bb a3 a5 f0 b6 c6 06 9d 25 +c1 e3 7c 66 4a bf f4 4f a3 52 12 19 75 60 26 ba +cd da 25 18 88 56 69 e5 be 75 f8 b3 85 4d 07 4f +d1 37 07 6d a0 79 ad a2 55 42 5e 19 6c 9e bb 9d +dd 7b ea 7b 0c e3 7f 59 f1 b0 0d 5f ff 73 ba d3 +2b 6e ca a4 b8 3e fd ae 8c 51 b8 bb 00 a0 de bd +98 2d cd ec 8f e4 26 24 f9 d8 3c diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 47c2087..9c52a22 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -9,7 +9,7 @@ import { import { HDLC_TELEGRAM_SOF_EOF, HDLC_FORMAT_START } from '../src/protocols/hdlc.js'; import { calculateCrc16IbmSdlc } from '../src/util/crc.js'; -export const TEST_DECRYPTION_KEY = Buffer.from('0123456789abcdef01234567890abcdef', 'hex'); +export const TEST_DECRYPTION_KEY = Buffer.from('0123456789abcdef0123456789abcdef', 'hex'); export const TEST_AAD = Buffer.from('ffeeddccbbaa99887766554433221100', 'hex'); export const DSMR_TEST_FOLDER = './tests/telegrams/dsmr'; export const DLMS_TEST_FOLDER = './tests/telegrams/dlms'; diff --git a/tsconfig.json b/tsconfig.json index f4c4023..846114c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./node_modules/@tsconfig/node18/tsconfig.json", - "include": ["src/**/*.ts", "tools/*.ts"], + "include": ["src/**/*.ts"], "compilerOptions": { "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */, From bf4f22ca358f7c47111371d921925c40c93cff07 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:23:14 +0200 Subject: [PATCH 12/18] 2.0.0-0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b878498..16699aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athombv/dsmr-parser", - "version": "1.2.2", + "version": "2.0.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athombv/dsmr-parser", - "version": "1.2.2", + "version": "2.0.0-0", "license": "ISC", "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/package.json b/package.json index d14416f..ccae678 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athombv/dsmr-parser", - "version": "1.2.2", + "version": "2.0.0-0", "description": "DSMR Parser for Smart Meters", "type": "module", "main": "dist/index.js", From 07c79cbcd0a27bf28b843d9faf63d01a5b87550a Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:24:33 +0200 Subject: [PATCH 13/18] chore: formatting --- examples/homey-energy-dongle-usb.js | 10 +++++--- examples/homey-energy-dongle-ws.js | 36 +++++++++++------------------ src/protocols/dlms-datatype.ts | 2 +- src/stream/stream-dlms.ts | 4 +--- src/stream/stream-encrypted-dsmr.ts | 4 +--- tools/parse-telegram.ts | 4 ++-- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/examples/homey-energy-dongle-usb.js b/examples/homey-energy-dongle-usb.js index c92ae38..f15515d 100644 --- a/examples/homey-energy-dongle-usb.js +++ b/examples/homey-energy-dongle-usb.js @@ -10,7 +10,11 @@ * The script will automatically detect the Homey Energy Dongle and start parsing data from from your Smart Meter! */ import { SerialPort } from 'serialport'; -import { DlmsStreamParser, UnencryptedDSMRStreamParser, EncryptedDSMRStreamParser } from '@athombv/dsmr-parser'; +import { + DlmsStreamParser, + UnencryptedDSMRStreamParser, + EncryptedDSMRStreamParser, +} from '@athombv/dsmr-parser'; const MODE = process.argv[2]; let serialPortPath = process.argv[3]; @@ -91,7 +95,7 @@ const parser = (() => { }, }); } - + if (MODE === 'dsmr' && DECRYPTION_KEY) { return new EncryptedDSMRStreamParser({ stream, @@ -104,7 +108,7 @@ const parser = (() => { console.dir(result, { depth: Infinity }); } }, - }) + }); } return new DlmsStreamParser({ diff --git a/examples/homey-energy-dongle-ws.js b/examples/homey-energy-dongle-ws.js index e7f2a1c..bc5d7c5 100644 --- a/examples/homey-energy-dongle-ws.js +++ b/examples/homey-energy-dongle-ws.js @@ -14,20 +14,28 @@ */ import WebSocket, { createWebSocketStream } from 'ws'; -import { DlmsStreamParser, UnencryptedDSMRStreamParser, EncryptedDSMRStreamParser } from '@athombv/dsmr-parser'; +import { + DlmsStreamParser, + UnencryptedDSMRStreamParser, + EncryptedDSMRStreamParser, +} from '@athombv/dsmr-parser'; const ENERGY_DONGLE_IP = process.argv[2]; const MODE = process.argv[3]; const DECRYPTION_KEY = process.argv[4]; if (!ENERGY_DONGLE_IP) { - console.log('Usage: node examples/homey-energy-dongle-ws.js '); + console.log( + 'Usage: node examples/homey-energy-dongle-ws.js ', + ); console.log('No IP address provided.'); process.exit(1); } if (!MODE || (MODE !== 'dsmr' && MODE !== 'dlms')) { - console.log('Usage: node examples/homey-energy-dongle-ws.js '); + console.log( + 'Usage: node examples/homey-energy-dongle-ws.js ', + ); console.log('No valid mode provided. Use "dsmr" or "dlms".'); process.exit(1); } @@ -127,7 +135,7 @@ while (true) { }, }); } - + if (MODE === 'dsmr' && DECRYPTION_KEY) { return new EncryptedDSMRStreamParser({ stream, @@ -140,7 +148,7 @@ while (true) { console.dir(result, { depth: Infinity }); } }, - }) + }); } return new DlmsStreamParser({ @@ -156,24 +164,6 @@ while (true) { }, }); })(); - // const parser = DSMR.createStreamParser({ - // stream, - // decryptionKey: DECRYPTION_KEY, - // detectEncryption: true, - // callback: (error, result) => { - // if (error instanceof DSMRError) { - // console.error('Error parsing DSMR data:', error.message); - // console.error('Raw data:', error.rawTelegram?.toString('hex')); - // } else if (error) { - // console.error('Error:', error); - // } else { - // // Not very useful to log the raw telegram here as it is already logged by the data listener on the stream. - // delete result.raw; - // console.log('Parsed telegram:'); - // console.dir(result, { depth: Infinity }); - // } - // }, - // }); // Don't continue the loop until the connection is closed. await new Promise((resolve) => { diff --git a/src/protocols/dlms-datatype.ts b/src/protocols/dlms-datatype.ts index 6d472e8..87eec9c 100644 --- a/src/protocols/dlms-datatype.ts +++ b/src/protocols/dlms-datatype.ts @@ -154,7 +154,7 @@ const parseStructureOrArray = (index: number, buffer: Buffer) => { * - A tag * - A Length (only for some data types) * - The value (length is either determined by the tag and length) - * + * * @note There are more data types. But these are the ones used by smart meters. */ export const DlmsDataTypes = new DlmsDataTypesInternal() diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index cfd85e7..778d8e2 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -29,9 +29,7 @@ export type DlmsStreamParserOptions = { * valid start of frame/header is received. */ fullFrameRequiredWithinMs?: number; - /** - * Data that is already available in the stream when the parser is created. - */ + /** Data that is already available in the stream when the parser is created. */ initialData?: Buffer; }; diff --git a/src/stream/stream-encrypted-dsmr.ts b/src/stream/stream-encrypted-dsmr.ts index 214e454..5240a2d 100644 --- a/src/stream/stream-encrypted-dsmr.ts +++ b/src/stream/stream-encrypted-dsmr.ts @@ -26,9 +26,7 @@ export type DSMRStreamParserOptions = Omit & { * valid start of frame/header is received. */ fullFrameRequiredWithinMs?: number; - /** - * Data that is already available in the stream when the parser is created. - */ + /** Data that is already available in the stream when the parser is created. */ initialData?: Buffer; }; diff --git a/tools/parse-telegram.ts b/tools/parse-telegram.ts index 59747b1..9d01de1 100644 --- a/tools/parse-telegram.ts +++ b/tools/parse-telegram.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { PassThrough } from 'node:stream'; import { SmartMeterDecryptionError, SmartMeterError } from '../src/index.js'; -import { StreamDetectType } from '../src/stream/stream-detect-type.js'; +import { SmartMeterDetectTypeStream } from '../src/stream/stream-detect-type.js'; import { DlmsStreamParser } from '../src/stream/stream-dlms.js'; import { EncryptedDSMRStreamParser } from '../src/stream/stream-encrypted-dsmr.js'; import { SmartMeterStreamCallback, SmartMeterStreamParser } from '../src/stream/stream.js'; @@ -46,7 +46,7 @@ const waitForFrameDetection = () => { encrypted: boolean; data: Buffer; }>((resolve) => { - const detector = new StreamDetectType({ + const detector = new SmartMeterDetectTypeStream({ stream: passthrough, callback: (result) => { detector.destroy(); From caa896c4c1209830e2478cb1f85e3f84552e46b9 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:24:39 +0200 Subject: [PATCH 14/18] 2.0.0-1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16699aa..5fb2f9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athombv/dsmr-parser", - "version": "2.0.0-0", + "version": "2.0.0-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athombv/dsmr-parser", - "version": "2.0.0-0", + "version": "2.0.0-1", "license": "ISC", "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/package.json b/package.json index ccae678..8b162ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athombv/dsmr-parser", - "version": "2.0.0-0", + "version": "2.0.0-1", "description": "DSMR Parser for Smart Meters", "type": "module", "main": "dist/index.js", From 27bf9eadde6aa9d6052fce3ca85f9d9291bfa5fe Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:29:38 +0200 Subject: [PATCH 15/18] feat: add crc field to base result --- src/protocols/dsmr.ts | 2 ++ src/protocols/hdlc.ts | 3 --- src/stream/stream-dlms.ts | 4 +--- src/util/base-result.ts | 2 ++ tests/telegrams/dlms/aidon-example-1.json | 8 +++----- tests/telegrams/dlms/aidon-example-2-segmented.json | 8 +++----- tests/telegrams/dlms/aidon-example-2.json | 8 +++----- tests/telegrams/dlms/described-list.json | 8 +++----- tests/telegrams/dlms/kamstrup-example-1.json | 8 +++----- tests/telegrams/dlms/kamstrup-example-2.json | 8 +++----- tests/telegrams/dlms/kamstrup-example-3.json | 8 +++----- tests/telegrams/dsmr/dsmr-4.0-isk-1.json | 3 ++- tests/telegrams/dsmr/dsmr-4.0-isk-2.json | 3 ++- tests/telegrams/dsmr/dsmr-4.0-spec-example.json | 3 ++- tests/telegrams/dsmr/dsmr-4.2-kfm-1.json | 3 ++- tests/telegrams/dsmr/dsmr-4.2-xmx-1.json | 3 ++- tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-ene-1.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-ene-2.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-ene-3.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-est-units.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-isk-1.json | 3 ++- .../telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json | 3 ++- tests/telegrams/dsmr/dsmr-5.0-spec-example.json | 3 ++- tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json | 3 ++- tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json | 3 ++- tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json | 3 ++- .../dsmr/kamstrup-OMNIA-e-meter-three-phase.json | 3 ++- tests/telegrams/dsmr/sagemcom-xt211.json | 3 ++- tools/parse-telegram.ts | 9 +-------- 30 files changed, 63 insertions(+), 67 deletions(-) diff --git a/src/protocols/dsmr.ts b/src/protocols/dsmr.ts index 05075b5..4d0915c 100644 --- a/src/protocols/dsmr.ts +++ b/src/protocols/dsmr.ts @@ -258,6 +258,8 @@ export const parseDsmr = (options: DsmrParserOptions): DsmrParserResult => { telegram, crc: result.dsmr.crc.value, }); + + result.crcValid = result.dsmr.crc.valid; } if (objectsParsed === 0) { diff --git a/src/protocols/hdlc.ts b/src/protocols/hdlc.ts index 0ff458a..25c3b0e 100644 --- a/src/protocols/hdlc.ts +++ b/src/protocols/hdlc.ts @@ -71,9 +71,6 @@ export type HdlcParserResult = BaseParserResult & { valid: boolean; }; }[]; - crc: { - valid: boolean; - }; }; dlms: { invokeId: number; diff --git a/src/stream/stream-dlms.ts b/src/stream/stream-dlms.ts index 778d8e2..08fffa4 100644 --- a/src/stream/stream-dlms.ts +++ b/src/stream/stream-dlms.ts @@ -186,9 +186,6 @@ export class DlmsStreamParser implements SmartMeterStreamParser { }, }; }), - crc: { - valid: allCrcValid, - }, }, // DLMS properties will be filled in by `decodeDlmsObis` dlms: { @@ -204,6 +201,7 @@ export class DlmsStreamParser implements SmartMeterStreamParser { electricity: {}, mBus: {}, metadata: {}, + crcValid: allCrcValid, }; if (this.options.decryptionKey) { diff --git a/src/util/base-result.ts b/src/util/base-result.ts index 8df59e7..2e7f6e4 100644 --- a/src/util/base-result.ts +++ b/src/util/base-result.ts @@ -91,4 +91,6 @@ export type BaseParserResult = { >; /** Only set when encryption is used */ additionalAuthenticatedDataValid?: boolean; + /** Only set when the frames contain a crc */ + crcValid?: boolean; }; diff --git a/tests/telegrams/dlms/aidon-example-1.json b/tests/telegrams/dlms/aidon-example-1.json index a2c10c5..35de34f 100644 --- a/tests/telegrams/dlms/aidon-example-1.json +++ b/tests/telegrams/dlms/aidon-example-1.json @@ -17,10 +17,7 @@ "value": 50400 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -58,5 +55,6 @@ "equipmentId": "7359992890941742" } }, - "metadata": {} + "metadata": {}, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2-segmented.json b/tests/telegrams/dlms/aidon-example-2-segmented.json index d01075d..d734a3c 100644 --- a/tests/telegrams/dlms/aidon-example-2-segmented.json +++ b/tests/telegrams/dlms/aidon-example-2-segmented.json @@ -45,10 +45,7 @@ "value": 11908 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -120,5 +117,6 @@ "mBus": {}, "metadata": { "timestamp": "07e30c1001073b28ff8000ff" - } + }, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dlms/aidon-example-2.json b/tests/telegrams/dlms/aidon-example-2.json index ac4724f..a0fc541 100644 --- a/tests/telegrams/dlms/aidon-example-2.json +++ b/tests/telegrams/dlms/aidon-example-2.json @@ -17,10 +17,7 @@ "value": 16574 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -92,5 +89,6 @@ "mBus": {}, "metadata": { "timestamp": "07e30c1001073b28ff8000ff" - } + }, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dlms/described-list.json b/tests/telegrams/dlms/described-list.json index 0f3b05a..9c13f12 100644 --- a/tests/telegrams/dlms/described-list.json +++ b/tests/telegrams/dlms/described-list.json @@ -17,10 +17,7 @@ "value": 55223 } } - ], - "crc": { - "valid": false - } + ] }, "dlms": { "invokeId": 0, @@ -82,5 +79,6 @@ } }, "mBus": {}, - "metadata": {} + "metadata": {}, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-1.json b/tests/telegrams/dlms/kamstrup-example-1.json index 113ac54..100f91c 100644 --- a/tests/telegrams/dlms/kamstrup-example-1.json +++ b/tests/telegrams/dlms/kamstrup-example-1.json @@ -17,10 +17,7 @@ "value": 58715 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -61,5 +58,6 @@ } }, "mBus": {}, - "metadata": {} + "metadata": {}, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-2.json b/tests/telegrams/dlms/kamstrup-example-2.json index e8b6ab7..7d5e966 100644 --- a/tests/telegrams/dlms/kamstrup-example-2.json +++ b/tests/telegrams/dlms/kamstrup-example-2.json @@ -17,10 +17,7 @@ "value": 34504 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -71,5 +68,6 @@ } }, "mBus": {}, - "metadata": {} + "metadata": {}, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dlms/kamstrup-example-3.json b/tests/telegrams/dlms/kamstrup-example-3.json index 9bcc8eb..5fae340 100644 --- a/tests/telegrams/dlms/kamstrup-example-3.json +++ b/tests/telegrams/dlms/kamstrup-example-3.json @@ -17,10 +17,7 @@ "value": 8453 } } - ], - "crc": { - "valid": true - } + ] }, "dlms": { "invokeId": 0, @@ -55,5 +52,6 @@ } }, "mBus": {}, - "metadata": {} + "metadata": {}, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json index c83f73c..36de26a 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-1.json @@ -88,5 +88,6 @@ "value": 12785.123, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json index e219990..d9136c1 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-isk-2.json +++ b/tests/telegrams/dsmr/dsmr-4.0-isk-2.json @@ -67,5 +67,6 @@ "powerReceivedTotal": 0, "powerReturnedTotal": 0 }, - "mBus": {} + "mBus": {}, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json index c9a5791..6b3c5c5 100644 --- a/tests/telegrams/dsmr/dsmr-4.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.0-spec-example.json @@ -88,5 +88,6 @@ "value": 12785.123, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json index 7de80db..58f8f6b 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-kfm-1.json @@ -108,5 +108,6 @@ "value": 5359.919, "unit": "m3" } - } + }, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json index c92e2c1..96bb998 100644 --- a/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json +++ b/tests/telegrams/dsmr/dsmr-4.2-xmx-1.json @@ -88,5 +88,6 @@ "value": 1234, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json index bd47e83..3fb75f4 100644 --- a/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-4.2.2-spec-example.json @@ -94,5 +94,6 @@ "value": 12785.123, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json index 93bd1d2..e0a837a 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-1.json @@ -103,5 +103,6 @@ "l3": 0.257 } }, - "mBus": {} + "mBus": {}, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json index 7395554..b7144ff 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-2.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-2.json @@ -114,5 +114,6 @@ "value": 1.29, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json index 7ba08a5..ac8b2c8 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-ene-3.json +++ b/tests/telegrams/dsmr/dsmr-5.0-ene-3.json @@ -113,5 +113,6 @@ "value": 6362.12, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-est-units.json b/tests/telegrams/dsmr/dsmr-5.0-est-units.json index ff16556..601120f 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-est-units.json +++ b/tests/telegrams/dsmr/dsmr-5.0-est-units.json @@ -56,5 +56,6 @@ "powerReceivedTotal": 0, "powerReturnedTotal": 3996 }, - "mBus": {} + "mBus": {}, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json index 1d21c69..042b2d2 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-isk-1.json +++ b/tests/telegrams/dsmr/dsmr-5.0-isk-1.json @@ -114,5 +114,6 @@ "value": 1569.646, "unit": "m3" } - } + }, + "crcValid": true } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json index 3222956..0f261d8 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example-lowercase.json @@ -114,5 +114,6 @@ "value": 12785.123, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json index 0068d17..10dfbfa 100644 --- a/tests/telegrams/dsmr/dsmr-5.0-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-5.0-spec-example.json @@ -114,5 +114,6 @@ "value": 12785.123, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json index 6764d09..1c0bbcb 100644 --- a/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json +++ b/tests/telegrams/dsmr/dsmr-luxembourgh-spec-example.json @@ -149,5 +149,6 @@ "value": 28.103, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json index 5cc2e31..b341234 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-1.json @@ -113,5 +113,6 @@ "value": 872.234, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json index a27865f..d37161c 100644 --- a/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json +++ b/tests/telegrams/dsmr/emucs-p1-v2.1.1-spec-example-2.json @@ -96,5 +96,6 @@ "value": 872.234, "unit": "m3" } - } + }, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json index 3cfd7f1..a7abb79 100644 --- a/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json +++ b/tests/telegrams/dsmr/kamstrup-OMNIA-e-meter-three-phase.json @@ -74,5 +74,6 @@ "l3": 4.6 } }, - "mBus": {} + "mBus": {}, + "crcValid": false } \ No newline at end of file diff --git a/tests/telegrams/dsmr/sagemcom-xt211.json b/tests/telegrams/dsmr/sagemcom-xt211.json index db7369a..e020493 100644 --- a/tests/telegrams/dsmr/sagemcom-xt211.json +++ b/tests/telegrams/dsmr/sagemcom-xt211.json @@ -107,5 +107,6 @@ "l3": 1.148 } }, - "mBus": {} + "mBus": {}, + "crcValid": false } \ No newline at end of file diff --git a/tools/parse-telegram.ts b/tools/parse-telegram.ts index 9d01de1..16f09c9 100644 --- a/tools/parse-telegram.ts +++ b/tools/parse-telegram.ts @@ -82,14 +82,7 @@ const callback: SmartMeterStreamCallback = (error, result) => { } else if (!result) { console.error('No result and no error'); } else { - let crcValid = true; - if ('hdlc' in result) { - crcValid = result.hdlc.crc.valid !== false; - } else if ('dsmr' in result) { - crcValid = result.dsmr.crc?.valid !== false; - } - - if (!crcValid) { + if (!result.crcValid) { console.error('CRC validation failed'); } console.dir(result, { depth: null }); From 0070ca9a9a6a26dd9d5df80cb11eaae2703c48db Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Tue, 3 Jun 2025 13:29:46 +0200 Subject: [PATCH 16/18] 2.0.0-2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fb2f9c..a6c543e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athombv/dsmr-parser", - "version": "2.0.0-1", + "version": "2.0.0-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athombv/dsmr-parser", - "version": "2.0.0-1", + "version": "2.0.0-2", "license": "ISC", "devDependencies": { "@eslint/js": "^9.8.0", diff --git a/package.json b/package.json index 8b162ca..edab8c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athombv/dsmr-parser", - "version": "2.0.0-1", + "version": "2.0.0-2", "description": "DSMR Parser for Smart Meters", "type": "module", "main": "dist/index.js", From b11897295c061914413cdfefa5491d9e3e824298 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 16 Jun 2025 11:56:21 +0200 Subject: [PATCH 17/18] chore: update readme and add discover example --- README.md | 119 ++++++---------------------------- examples/discover-protocol.js | 57 ++++++++++++++++ 2 files changed, 76 insertions(+), 100 deletions(-) create mode 100644 examples/discover-protocol.js diff --git a/README.md b/README.md index e9d49fb..652c5f7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,18 @@ [![Test](https://github.com/athombv/node-dsmr-parser/actions/workflows/test.yml/badge.svg)](https://github.com/athombv/node-dsmr-parser/actions/workflows/test.yml) [![Build](https://github.com/athombv/node-dsmr-parser/actions/workflows/build.yml/badge.svg)](https://github.com/athombv/node-dsmr-parser/actions/workflows/build.yml) -This module can parse Dutch Smart Meter Requirements (DSMR) messages, and return their contents as JavaScript Objects. +This module can parse Smart Meter P1 messages, and return their contents as JavaScript Objects. + +This module supports the following protocols: + +- DSMR (Dutch Smart Meter Requirements) + - Or derivatives of DSMR. Such as: + - ESMR (European Smart Meter Requirements) + - eMUCS P1 (Belgian Smart Meters) + - E Meter P1 (Luxembourg's Smart Meters) +- DLMS/COSEM: + - Using HDLC as transport layer + - Can be used by Smart Meters in the Nordics ## Installation @@ -13,104 +24,12 @@ $ npm i @athombv/dsmr-parser ## Examples -### Parsing a DSMR frame - -```javascript -import { DSMR } from '@athombv/dsmr-parser'; - -try { - const result = DSMR.parse({ - telegram: `/ISk5\2MT382-1000 - -1-3:0.2.8(50) -0-0:1.0.0(101209113020W) -0-0:96.1.1(4B384547303034303436333935353037) -1-0:1.8.1(123456.789*kWh) -1-0:1.8.2(123456.789*kWh) -1-0:2.8.1(123456.789*kWh) -1-0:2.8.2(123456.789*kWh) -0-0:96.14.0(0002) -1-0:1.7.0(01.193*kW) -1-0:2.7.0(00.000*kW) -0-0:96.7.21(00004) -0-0:96.7.9(00002) -1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s) -1-0:32.32.0(00002) -1-0:52.32.0(00001) -1-0:72.32.0(00000) -1-0:32.36.0(00000) -1-0:52.36.0(00003) -1-0:72.36.0(00000) -0-0:96.13.0(303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F) -1-0:32.7.0(220.1*V) -1-0:52.7.0(220.2*V) -1-0:72.7.0(220.3*V) -1-0:31.7.0(001*A) -1-0:51.7.0(002*A) -1-0:71.7.0(003*A) -1-0:21.7.0(01.111*kW) -1-0:41.7.0(02.222*kW) -1-0:61.7.0(03.333*kW) -1-0:22.7.0(04.444*kW) -1-0:42.7.0(05.555*kW) -1-0:62.7.0(06.666*kW) -0-1:24.1.0(003) -0-1:96.1.0(3232323241424344313233343536373839) -0-1:24.2.1(101209112500W)(12785.123*m3) -!EF2F -`, - decryptionKey: '...', // Only for Luxembourg - }); - - console.log('Result:', result); -} catch (err) { - console.error(`Error Parsing DSMR Telegram: ${err.message}`); -} -``` +To learn how to parse a data frames from a Smart Meter, please checkout the examples in the [`examples`](./examples/) directory. -Will result in the following log: - -```log -Result: { - header: { identifier: '\\2MT382-1000r', xxx: 'ISk', z: '5' }, - metadata: { - dsmrVersion: 5, - timestamp: '101209113020W', - equipmentId: '4B384547303034303436333935353037', - events: { - powerFailures: 4, - longPowerFailures: 2, - voltageSags: { l1: 2, l2: 1, l3: 0 }, - voltageSwells: { l1: 0, l2: 3, l3: 0 } - }, - unknownLines: [ - '1-0:99.97.0(2)(0-0:96.7.19)(101208152415W)(0000000240*s)(101208151004W)(0000000301*s)' - ], - textMessage: '303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F303132333435363738393A3B3C3D3E3F' - }, - electricity: { - tariff1: { received: 123456.789, returned: 123456.789 }, - tariff2: { received: 123456.789, returned: 123456.789 }, - currentTariff: 2, - powerReturnedTotal: 1.193, - powerReceivedTotal: 0, - voltage: { l1: 220.1, l2: 220.2, l3: 220.3 }, - current: { l1: 1, l2: 2, l3: 3 }, - powerReturned: { l1: 1.111, l2: 2.222, l3: 3.333 }, - powerReceived: { l1: 4.444, l2: 5.555, l3: 6.666 } - }, - mBus: { - '1': { - deviceType: 3, - equipmentId: '3232323241424344313233343536373839', - timestamp: '101209112500W', - value: 12785.123, - unit: 'm3' - } - }, - crc: { value: 61231, valid: false } -} -``` +### Detecting the user protocol + +If you don't know which protocol is used by your Smart Meter, it is possible to detect which protocol is used. +An example of how to do this is located in [`examples/discover-protocol.js`](./examples/discover-protocol.js). ### Connecting Homey Energy Dongle using USB @@ -136,7 +55,7 @@ npm run build 5. Connect Homey Energy Dongle to a Smart Meter 6. Connect the USB-C port of Homey Energy Dongle to your PC 7. Run the example script: - - Replace `` with either `dsmr` or `dlms`. + - Replace `` with either `dsmr` or `dlms` to use either the DSMR or DLMS/COSEM protocol. ```sh node examples/homey-energy-dongle-usb.js @@ -174,7 +93,7 @@ npm run build 7. Enable the Local API in Homey Energy Dongle's settings in Homey - You can also find Homey Energy Dongle's IP address here 8. Run the example script: - - `mode` must be either `dsmr` or `dlms`. + - `mode` must be either `dsmr` or `dlms` to use either the DSMR or DLMS/COSEM protocol. ```sh node examples/homey-energy-dongle-ws.js diff --git a/examples/discover-protocol.js b/examples/discover-protocol.js new file mode 100644 index 0000000..483b725 --- /dev/null +++ b/examples/discover-protocol.js @@ -0,0 +1,57 @@ +import { SerialPort } from 'serialport'; +import { SmartMeterDetectTypeStream } from '@athombv/dsmr-parser'; + +const SERIAL_PORT_PATH = process.argv[2]; + +if (!SERIAL_PORT_PATH) { + console.log('Usage: node examples/discover-protocol.js '); + console.log('No serial port path provided.'); + process.exit(1); +} + +console.log(`Discovering protocol on serial port: ${SERIAL_PORT_PATH}`); + +const serialPort = new SerialPort( + { + path: SERIAL_PORT_PATH, + baudRate: 115200, + }, + (err) => { + if (err) { + console.error('Error opening port:', err); + process.exit(1); + } else { + console.log('Waiting for data on the serial port...'); + } + }, +); + +const discoveryParser = new SmartMeterDetectTypeStream({ + stream: serialPort, + callback: (result) => { + console.log(`Detected protocol type: ${result.mode}`); + console.log(`Is ${result.encrypted ? '' : 'not '}using encryption`); + + discoveryParser.destroy(); + + serialPort.close((err) => { + if (err) { + console.error('Error closing port:', err); + } else { + console.log('Serial port closed.'); + } + process.exit(0); + }); + }, +}); + +setTimeout(() => { + console.log('No protocol detected within 30 seconds. Exiting...'); + discoveryParser.destroy(); + serialPort.close((err) => { + if (err) { + console.error('Error closing port:', err); + } + }); + process.exit(0); +}, 30000); From 6e6054f98335cff2ab0d3a745bd2d45f9c66cc99 Mon Sep 17 00:00:00 2001 From: Niels de Boer Date: Mon, 16 Jun 2025 11:59:16 +0200 Subject: [PATCH 18/18] chore: cleanup --- examples/discover-protocol.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/discover-protocol.js b/examples/discover-protocol.js index 483b725..f4e4528 100644 --- a/examples/discover-protocol.js +++ b/examples/discover-protocol.js @@ -37,9 +37,8 @@ const discoveryParser = new SmartMeterDetectTypeStream({ serialPort.close((err) => { if (err) { console.error('Error closing port:', err); - } else { - console.log('Serial port closed.'); } + process.exit(0); }); }, @@ -52,6 +51,7 @@ setTimeout(() => { if (err) { console.error('Error closing port:', err); } + + process.exit(0); }); - process.exit(0); }, 30000);