From 8e9da8f25531f9df0f1835201d6136f0570f29ac Mon Sep 17 00:00:00 2001 From: Gunnar Skjold Date: Sat, 6 Nov 2021 16:56:02 +0100 Subject: [PATCH] First step in implementing a new DLMS parser --- doc/Aidon_OBIS.txt | 18 ++ doc/Kamstrup_encrypted_OBIS.txt | 32 +++ frames/Aidon-TN-3p.raw | 4 +- platformio.ini | 5 +- src/AmsData.cpp | 451 ++++++++++++-------------------- src/AmsData.h | 1 + src/AmsToMqttBridge.ino | 84 ++++++ src/ams/ams.cpp | 119 +++++++++ src/ams/ams.h | 34 +++ src/ams/crc.cpp | 12 + src/ams/crc.h | 9 + src/ams/hdlc.cpp | 138 ++++++++++ src/ams/hdlc.h | 94 +++++++ 13 files changed, 719 insertions(+), 282 deletions(-) create mode 100644 doc/Aidon_OBIS.txt create mode 100644 doc/Kamstrup_encrypted_OBIS.txt create mode 100644 src/ams/ams.cpp create mode 100644 src/ams/ams.h create mode 100644 src/ams/crc.cpp create mode 100644 src/ams/crc.h create mode 100644 src/ams/hdlc.cpp create mode 100644 src/ams/hdlc.h diff --git a/doc/Aidon_OBIS.txt b/doc/Aidon_OBIS.txt new file mode 100644 index 00000000..ad091abc --- /dev/null +++ b/doc/Aidon_OBIS.txt @@ -0,0 +1,18 @@ +1.1.0.2.129.255 +0.0.96.1.0.255 +0.0.96.1.7.255 +1.0.1.7.0.255 - Active+ Instantaneous value +1.0.2.7.0.255 - Active- Instantaneous value +1.0.3.7.0.255 - Reactive+ Instantaneous value +1.0.4.7.0.255 - Reactive- Instantaneous value +1.0.31.7.0.255 - L1 Current Instantaneous value +1.0.51.7.0.255 - L2 Current Instantaneous value +1.0.71.7.0.255 - L3 Current Instantaneous value +1.0.32.7.0.255 - L1 Voltage Instantaneous value +1.0.52.7.0.255 - L2 Voltage Instantaneous value +1.0.72.7.0.255 - L3 Voltage Instantaneous value +0.0.1.0.0.255 - Current date/time +1.0.1.8.0.255 - Active+ Energy +1.0.2.8.0.255 - Active- Energy +1.0.3.8.0.255 - Reactive+ Energy +1.0.4.8.0.255 - Reactive- Energy diff --git a/doc/Kamstrup_encrypted_OBIS.txt b/doc/Kamstrup_encrypted_OBIS.txt new file mode 100644 index 00000000..346a3483 --- /dev/null +++ b/doc/Kamstrup_encrypted_OBIS.txt @@ -0,0 +1,32 @@ +1.1.1.8.0.255 - Active+ Energy +1.1.2.8.0.255 - Active- Energy +1.1.3.8.0.255 - Reactive+ Energy +1.1.4.8.0.255 - Reactive- Energy +1.1.0.0.1.255 +1.1.1.7.0.255 - Active+ Instantaneous value +1.1.2.7.0.255 - Active- Instantaneous value +1.1.3.7.0.255 - Reactive+ Instantaneous value +1.1.4.7.0.255 - Reactive- Instantaneous value +0.1.1.0.0.255 +1.1.32.7.0.255 - L1 Voltage Instantaneous value +1.1.52.7.0.255 - L2 Voltage Instantaneous value +1.1.72.7.0.255 - L3 Voltage Instantaneous value +1.1.31.7.0.255 - L1 Current Instantaneous value +1.1.51.7.0.255 - L2 Current Instantaneous value +1.1.71.7.0.255 - L3 Current Instantaneous value +1.1.21.7.0.255 - L1 Active+ Instantaneous value +1.1.41.7.0.255 - L2 Active+ Instantaneous value +1.1.61.7.0.255 - L3 Active+ Instantaneous value +1.1.33.7.0.255 - L1 (cos.phi) (PF) Instantaneous value +1.1.53.7.0.255 - L2 (cos.phi) (PF) Instantaneous value +1.1.73.7.0.255 - L3 (cos.phi) (PF) Instantaneous value +1.1.13.7.0.255 - Avegage (cos.phi) (PF) Inst. value +1.1.22.7.0.255 - L1 Active- Instantaneous value +1.1.42.7.0.255 - L2 Active- Instantaneous value +1.1.62.7.0.255 - L3 Active- Instantaneous value +1.1.22.8.0.255 - L1 Active- Energy +1.1.42.8.0.255 - L2 Active- Energy +1.1.62.8.0.255 - L3 Active- Energy +1.1.21.8.0.255 - L1 Active+ Energy +1.1.41.8.0.255 - L2 Active+ Energy +1.1.61.8.0.255 - L3 Active+ Energy diff --git a/frames/Aidon-TN-3p.raw b/frames/Aidon-TN-3p.raw index c6d723cb..0296e2cb 100644 --- a/frames/Aidon-TN-3p.raw +++ b/frames/Aidon-TN-3p.raw @@ -1,4 +1,6 @@ -T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD + T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 64 02 02 0F 00 16 1B E1 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 6C 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 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 09 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 41 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 13 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F2 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 D1 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 E8 02 02 0F FF 16 23 8B 7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 03 9A 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 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 0E 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 10 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F4 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 CD 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 DC 02 02 0F FF 16 23 02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 03 18 03 08 00 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06 00 47 F0 34 02 02 0F 01 16 1E 02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02 03 09 06 01 00 03 08 00 FF 06 00 00 21 9E 02 02 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00 08 E0 21 02 02 0F 01 16 20 57 +7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 09 6D 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 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 5B 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 03 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 5A 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 04 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 09 02 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 EC 02 02 0F FF 16 23 02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 0A 1F 00 14 00 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06 00 56 9F 52 02 02 0F 01 16 1E 02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02 03 09 06 01 00 03 08 00 FF 06 00 00 22 D0 02 02 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00 0A F5 EC 02 02 0F 01 16 20 51 D7 7E +7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 09 56 02 02 0F 00 16 1B AF A1 7E diff --git a/platformio.ini b/platformio.ini index 869662b6..9e05c238 100755 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,8 @@ extra_configs = platformio-user.ini [common] -lib_deps = file://lib/HanReader, file://lib/Timezone, MQTT@2.5.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git@3.0.5, Time@1.6.0 +lib_deps = file://lib/Timezone, MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git@3.0.5, Time@1.6.0 +lib_ignore = OneWire [env:esp8266] platform = espressif8266@3.2.0 @@ -10,6 +11,7 @@ board = esp12e board_build.ldscript = eagle.flash.4m2m.ld framework = arduino lib_deps = ${common.lib_deps} +lib_ignore = ${common.lib_ignore} extra_scripts = pre:scripts/addversion.py scripts/makeweb.py @@ -22,6 +24,7 @@ platform = espressif32@3.3.2 board = esp32dev framework = arduino lib_deps = ${common.lib_deps} +lib_ignore = ${common.lib_ignore} extra_scripts = pre:scripts/addversion.py scripts/makeweb.py diff --git a/src/AmsData.cpp b/src/AmsData.cpp index 82810dcd..52d642a5 100644 --- a/src/AmsData.cpp +++ b/src/AmsData.cpp @@ -1,30 +1,171 @@ #include "AmsData.h" -#include "Kaifa.h" -#include "Aidon.h" -#include "Kamstrup.h" -#include "Omnipower.h" +#include "ams/ams.h" + +uint8_t AMS_OBIS_VERSION[6] = { 1, 1, 0, 2, 129, 255 }; +uint8_t AMS_OBIS_METER_MODEL[6] = { 0, 0, 96, 1, 7, 255 }; +uint8_t AMS_OBIS_METER_ID[6] = { 0, 0, 96, 1, 0, 255 }; +uint8_t AMS_OBIS_METER_TIMESTAMP[6] = { 0, 0, 1, 0, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_IMPORT[6] = { 1, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_IMPORT_L1[6] = { 21, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_IMPORT_L2[6] = { 41, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_IMPORT_L3[6] = { 61, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_EXPORT[6] = { 2, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_EXPORT_L1[6] = { 22, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_EXPORT_L2[6] = { 42, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_EXPORT_L3[6] = { 62, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_IMPORT[6] = { 3, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_IMPORT_L1[6] = { 23, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_IMPORT_L2[6] = { 43, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_IMPORT_L3[6] = { 63, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_EXPORT[6] = { 4, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_EXPORT_L1[6] = { 24, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_EXPORT_L2[6] = { 44, 7, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_EXPORT_L3[6] = { 64, 7, 0, 255 }; +uint8_t AMS_OBIS_CURRENT[6] = { 11, 7, 0, 255 }; +uint8_t AMS_OBIS_CURRENT_L1[6] = { 31, 7, 0, 255 }; +uint8_t AMS_OBIS_CURRENT_L2[6] = { 51, 7, 0, 255 }; +uint8_t AMS_OBIS_CURRENT_L3[6] = { 71, 7, 0, 255 }; +uint8_t AMS_OBIS_VOLTAGE[6] = { 12, 7, 0, 255 }; +uint8_t AMS_OBIS_VOLTAGE_L1[6] = { 32, 7, 0, 255 }; +uint8_t AMS_OBIS_VOLTAGE_L2[6] = { 52, 7, 0, 255 }; +uint8_t AMS_OBIS_VOLTAGE_L3[6] = { 72, 7, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_IMPORT_COUNT[6] = { 1, 8, 0, 255 }; +uint8_t AMS_OBIS_ACTIVE_EXPORT_COUNT[6] = { 2, 8, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_IMPORT_COUNT[6] = { 3, 8, 0, 255 }; +uint8_t AMS_OBIS_REACTIVE_EXPORT_COUNT[6] = { 4, 8, 0, 255 }; AmsData::AmsData() {} -AmsData::AmsData(uint8_t meterType, bool substituteMissing, HanReader& hanReader) { - lastUpdateMillis = millis(); - packageTimestamp = hanReader.getPackageTime(true, true); +AmsData::AmsData(const char* d, bool substituteMissing) { + uint32_t u32; + int32_t s32; + char str[64]; - int listSize = hanReader.getListSize(); - switch(meterType) { - case METER_TYPE_KAIFA: - extractFromKaifa(hanReader, listSize); - break; - case METER_TYPE_AIDON: - extractFromAidon(hanReader, listSize); - break; - case METER_TYPE_KAMSTRUP: - extractFromKamstrup(hanReader, listSize); - break; - case METER_TYPE_OMNIPOWER: - extractFromOmnipower(hanReader, listSize); - break; + u32 = AMS_getUnsignedNumber(AMS_OBIS_ACTIVE_IMPORT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 1; + activeImportPower = u32; } + + int meterType = AmsTypeUnknown; + CosemData* version = AMS_findObis(AMS_OBIS_VERSION, d); + if(version != NULL && version->base.type == CosemTypeString) { + if(memcmp(version->str.data, "AIDON", 5) == 0) { + meterType = AmsTypeAidon; + } else if(memcmp(version->str.data, "Kamstrup", 8) == 0) { + meterType = AmsTypeKamstrup; + } + } + + u32 = AMS_getString(AMS_OBIS_VERSION, ((char *) (d)), str); + if(u32 > 0) { + listId = String(str); + } + + u32 = AMS_getUnsignedNumber(AMS_OBIS_ACTIVE_EXPORT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + activeExportPower = u32; + } + + u32 = AMS_getUnsignedNumber(AMS_OBIS_REACTIVE_IMPORT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + reactiveImportPower = u32; + } + + u32 = AMS_getUnsignedNumber(AMS_OBIS_REACTIVE_EXPORT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + reactiveExportPower = u32; + } + + u32 = AMS_getUnsignedNumber(AMS_OBIS_VOLTAGE_L1, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 2; + l1voltage = u32; + } + u32 = AMS_getUnsignedNumber(AMS_OBIS_VOLTAGE_L2, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 2; + l2voltage = u32; + } + u32 = AMS_getUnsignedNumber(AMS_OBIS_VOLTAGE_L3, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 2; + l3voltage = u32; + } + + s32 = AMS_getSignedNumber(AMS_OBIS_CURRENT_L1, ((char *) (d))); + if(s32 != 0xFFFFFFFF) { + listType = 2; + l1current = s32; + } + s32 = AMS_getSignedNumber(AMS_OBIS_CURRENT_L2, ((char *) (d))); + if(s32 != 0xFFFFFFFF) { + listType = 2; + l2current = s32; + } + s32 = AMS_getSignedNumber(AMS_OBIS_CURRENT_L3, ((char *) (d))); + if(s32 != 0xFFFFFFFF) { + listType = 2; + l3current = s32; + } + + int vdiv = 1; + int voltage = l1voltage == 0 ? l2voltage == 0 ? l3voltage == 0 ? 0 : l3voltage : l2voltage : l1voltage; + while(voltage > 1000) { + vdiv *= 10; + voltage /= 10; + } + + l1voltage = l1voltage != 0 ? l1voltage / vdiv : 0; + l2voltage = l2voltage != 0 ? l2voltage / vdiv : 0; + l3voltage = l3voltage != 0 ? l3voltage / vdiv : 0; + + if(meterType == AmsTypeAidon) { + l1current = l1current != 0 ? l1current / 10.0 : 0; + l2current = l2current != 0 ? l2current / 10.0 : 0; + l3current = l3current != 0 ? l3current / 10.0 : 0; + } else if(meterType == AmsTypeKamstrup) { + l1current = l1current != 0 ? l1current / 100.0 : 0; + l2current = l2current != 0 ? l2current / 100.0 : 0; + l3current = l3current != 0 ? l3current / 100.0 : 0; + } + + u32 = AMS_getUnsignedNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 3; + activeImportCounter = u32 / 100.0; + } + u32 = AMS_getUnsignedNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 3; + activeExportCounter = u32 / 100.0; + } + u32 = AMS_getUnsignedNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 3; + reactiveImportCounter = u32 / 100.0; + } + u32 = AMS_getUnsignedNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, ((char *) (d))); + if(u32 != 0xFFFFFFFF) { + listType = 3; + reactiveExportCounter = u32 / 100.0; + } + + u32 = AMS_getString(AMS_OBIS_METER_MODEL, ((char *) (d)), str); + if(u32 > 0) { + meterModel = String(str); + } + + u32 = AMS_getString(AMS_OBIS_METER_ID, ((char *) (d)), str); + if(u32 > 0) { + meterId = String(str); + } + + time_t ts = AMS_getTimestamp(AMS_OBIS_METER_TIMESTAMP, ((char *) (d))); + if(ts > 0) { + meterTimestamp = ts; + } + threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0; twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0); @@ -33,267 +174,17 @@ AmsData::AmsData(uint8_t meterType, bool substituteMissing, HanReader& hanReader l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage; } } -} -void AmsData::extractFromKaifa(HanReader& hanReader, uint8_t listSize) { - switch(listSize) { - case (uint8_t)Kaifa::List1: - listType = 1; - break; - case (uint8_t)Kaifa::List3PhaseShort: - threePhase = true; - case (uint8_t)Kaifa::List1PhaseShort: - listType = 2; - break; - case (uint8_t)Kaifa::List3PhaseLong: - threePhase = true; - case (uint8_t)Kaifa::List1PhaseLong: - listType = 3; - break; +/* + time_t packageTimestamp = 0; +*/ +/* + CosemData* model = AMS_findObis(AMS_OBIS_METER_MODEL, ((char *) (d))); + if(model != NULL && model->base.type == CosemTypeString) { + this->meterModel = String((const char*) model->str.data); } - - if(listSize == (uint8_t)Kaifa::List1) { - activeImportPower = hanReader.getInt((int)Kaifa_List1::ActivePowerImported); - } else { - switch(listSize) { - case (uint8_t)Kaifa::List3PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock, false, false); - activeImportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveImportEnergy)) / 1000; - activeExportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveExportEnergy)) / 1000; - reactiveImportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeReactiveImportEnergy)) / 1000; - reactiveExportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeReactiveExportEnergy)) / 1000; - case (uint8_t)Kaifa::List3PhaseShort: - listId = hanReader.getString( (int)Kaifa_List3Phase::ListVersionIdentifier); - meterId = hanReader.getString( (int)Kaifa_List3Phase::MeterID); - meterModel = hanReader.getString( (int)Kaifa_List3Phase::MeterType); - activeImportPower = hanReader.getUint( (int)Kaifa_List3Phase::ActiveImportPower); - reactiveImportPower = hanReader.getUint( (int)Kaifa_List3Phase::ReactiveImportPower); - activeExportPower = hanReader.getUint( (int)Kaifa_List3Phase::ActiveExportPower); - reactiveExportPower = hanReader.getUint( (int)Kaifa_List3Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL1)) / 1000; - l2current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL2)) / 1000; - l3current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL3)) / 1000; - l1voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL1)) / 10; - l2voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL2)) / 10; - l3voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL3)) / 10; - break; - case (uint8_t)Kaifa::List1PhaseLong: - meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock, false, false); - activeImportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveImportEnergy)); - activeExportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveExportEnergy)); - reactiveImportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeReactiveImportEnergy)); - reactiveExportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeReactiveExportEnergy)); - case (uint8_t)Kaifa::List1PhaseShort: - listId = hanReader.getString( (int)Kaifa_List1Phase::ListVersionIdentifier); - meterId = hanReader.getString( (int)Kaifa_List1Phase::MeterID); - meterModel = hanReader.getString( (int)Kaifa_List1Phase::MeterType); - activeImportPower = hanReader.getUint( (int)Kaifa_List1Phase::ActiveImportPower); - reactiveImportPower = hanReader.getUint( (int)Kaifa_List1Phase::ReactiveImportPower); - activeExportPower = hanReader.getUint( (int)Kaifa_List1Phase::ActiveExportPower); - reactiveExportPower = hanReader.getUint( (int)Kaifa_List1Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt( (int)Kaifa_List1Phase::CurrentL1)) / 1000; - l1voltage = ((float) hanReader.getInt( (int)Kaifa_List1Phase::VoltageL1)) / 10; - break; - } - } -} - -void AmsData::extractFromAidon(HanReader& hanReader, uint8_t listSize) { - switch(listSize) { - case (uint8_t)Aidon::List1: - listType = 1; - break; - case (uint8_t)Aidon::List3PhaseITShort: - case (uint8_t)Aidon::List3PhaseShort: - threePhase = true; - case (uint8_t)Aidon::List1PhaseShort: - listType = 2; - break; - case (uint8_t)Aidon::List3PhaseITLong: - case (uint8_t)Aidon::List3PhaseLong: - threePhase = true; - case (uint8_t)Aidon::List1PhaseLong: - listType = 3; - break; - } - - if(listSize == (uint8_t)Aidon::List1) { - activeImportPower = hanReader.getUint((uint8_t)Aidon_List1::ActiveImportPower); - } else { - switch(listSize) { - case (uint8_t)Aidon::List3PhaseLong: - meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List3Phase::Timestamp, false, false); - activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Aidon::List3PhaseShort: - listId = hanReader.getString( (uint8_t)Aidon_List3Phase::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Aidon_List3Phase::MeterID); - meterModel = hanReader.getString( (uint8_t)Aidon_List3Phase::MeterType); - activeImportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ActiveImportPower); - reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ReactiveImportPower); - activeExportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ActiveExportPower); - reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL1)) / 10; - l2current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL2)) / 10; - l3current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL3)) / 10; - l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL1)) / 10; - l2voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL2)) / 10; - l3voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL3)) / 10; - break; - case (uint8_t)Aidon::List1PhaseLong: - meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List1Phase::Timestamp, false, false); - activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Aidon::List1PhaseShort: - listId = hanReader.getString( (uint8_t)Aidon_List1Phase::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Aidon_List1Phase::MeterID); - meterModel = hanReader.getString( (uint8_t)Aidon_List1Phase::MeterType); - activeImportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ActiveImportPower); - reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ReactiveImportPower); - activeExportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ActiveExportPower); - reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List1Phase::CurrentL1)) / 10; - l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List1Phase::VoltageL1)) / 10; - break; - case (uint8_t)Aidon::List3PhaseITLong: - meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List3PhaseIT::Timestamp, false, false); - activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Aidon::List3PhaseITShort: - listId = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::MeterID); - meterModel = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::MeterType); - activeImportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ActiveImportPower); - reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ReactiveImportPower); - activeExportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ActiveExportPower); - reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ReactiveExportPower); - l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::CurrentL1)) / 10; - l2current = 0; - l3current = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::CurrentL3)) / 10; - l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL1)) / 10; - l2voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL2)) / 10; - l3voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL3)) / 10; - break; - } - } -} - -void AmsData::extractFromKamstrup(HanReader& hanReader, uint8_t listSize) { - switch(listSize) { - case (uint8_t)Kamstrup::List3PhaseITShort: - case (uint8_t)Kamstrup::List3PhaseShort: - threePhase = true; - case (uint8_t)Kamstrup::List1PhaseShort: - listType = 2; - break; - case (uint8_t)Kamstrup::List3PhaseITLong: - case (uint8_t)Kamstrup::List3PhaseLong: - threePhase = true; - case (uint8_t)Kamstrup::List1PhaseLong: - listType = 3; - break; - } - - switch(listSize) { - case (uint8_t)Kamstrup::List1PhaseLong: - meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List1Phase::MeterClock, true, true); - activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Kamstrup::List1PhaseShort: - listId = hanReader.getString( (uint8_t)Kamstrup_List1Phase::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Kamstrup_List1Phase::MeterID); - meterModel = hanReader.getString( (uint8_t)Kamstrup_List1Phase::MeterType); - activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ActiveImportPower); - reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ReactiveImportPower); - activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ActiveExportPower); - reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CurrentL1)) / 100; - l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::VoltageL1); - break; - case (uint8_t)Kamstrup::List3PhaseLong: - meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List3Phase::MeterClock, true, true); - activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Kamstrup::List3PhaseShort: - listId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterID); - meterModel = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterType); - activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveImportPower); - reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveImportPower); - activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveExportPower); - reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL1)) / 100; - l2current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL2)) / 100; - l3current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL3)) / 100; - l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL1); - l2voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL2); - l3voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL3); - break; - case (uint8_t)Kamstrup::List3PhaseITLong: - meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List3Phase::MeterClock, true, true); - activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100; - case (uint8_t)Kamstrup::List3PhaseITShort: - listId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::ListVersionIdentifier); - meterId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterID); - meterModel = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterType); - activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveImportPower); - reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveImportPower); - activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveExportPower); - reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveExportPower); - l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL1)) / 100; - l2current = 0; - l3current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL3)) / 100; - l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL1); - l2voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL2); - l3voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL3); - break; - } -} - -void AmsData::extractFromOmnipower(HanReader& hanReader, uint8_t listSize) { - switch(listSize) { - case (uint8_t)Kamstrup::List3PhaseITShort: - case (uint8_t)Kamstrup::List3PhaseShort: - case (uint8_t)Kamstrup::List1PhaseShort: - case (uint8_t)Kamstrup::List3PhaseITLong: - case (uint8_t)Kamstrup::List3PhaseLong: - case (uint8_t)Kamstrup::List1PhaseLong: - extractFromKamstrup(hanReader, listSize); - break; - case (uint8_t)Omnipower::DLMS: - meterTimestamp = hanReader.getTime( (uint8_t)Omnipower_DLMS::MeterClock, true, true); - activeImportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeActiveImportEnergy)) / 100; - activeExportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeActiveExportEnergy)) / 100; - reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeReactiveImportEnergy)) / 100; - reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeReactiveExportEnergy)) / 100; - listId = hanReader.getString( (uint8_t)Omnipower_DLMS::ListVersionIdentifier); - activeImportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ActiveImportPower); - reactiveImportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ReactiveImportPower); - activeExportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ActiveExportPower); - reactiveExportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ReactiveExportPower); - l1current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL1)) / 100; - l2current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL2)) / 100; - l3current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL3)) / 100; - l1voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL1); - l2voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL2); - l3voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL3); - listType = 3; - break; - } - threePhase = l3voltage != 0; +*/ + lastUpdateMillis = millis(); } void AmsData::apply(AmsData& other) { diff --git a/src/AmsData.h b/src/AmsData.h index e6d22ce8..daf93dad 100644 --- a/src/AmsData.h +++ b/src/AmsData.h @@ -13,6 +13,7 @@ class AmsData { public: AmsData(); + AmsData(const char* d, bool substituteMissing); AmsData(uint8_t meterType, bool substituteMissing, HanReader& hanReader); void apply(AmsData& other); diff --git a/src/AmsToMqttBridge.ino b/src/AmsToMqttBridge.ino index 0fd42689..527523c6 100644 --- a/src/AmsToMqttBridge.ino +++ b/src/AmsToMqttBridge.ino @@ -47,6 +47,9 @@ ADC_MODE(ADC_VCC); #include "RemoteDebug.h" +#define BUF_SIZE (1024) +#include "ams/hdlc.h" + HwTools hw; DNSServer* dnsServer = NULL; @@ -597,8 +600,89 @@ void mqttMessageReceived(String &topic, String &payload) // Ideas could be to query for values or to initiate OTA firmware update } +HDLCConfig* hc = NULL; int currentMeterType = 0; void readHanPort() { + uint8_t buf[BUF_SIZE]; + size_t len = hanSerial->readBytes(buf, BUF_SIZE); // TODO: read one byte at the time. This blocks up the GUI + if(len > 0) { + if(meterConfig.type == 4 && hc == NULL) { + hc = new HDLCConfig(); + memcpy(hc->encryption_key, meterConfig.encryptionKey, 16); + memcpy(hc->authentication_key, meterConfig.authenticationKey, 16); + } + int pos = HDLC_validate((uint8_t *) buf, len, hc); + if(Debug.isActive(RemoteDebug::INFO)) { + debugD("Frame dump:"); + debugPrint(buf, 0, len); + if(hc != NULL) { + debugD("System title:"); + debugPrint(hc->system_title, 0, 8); + debugD("Initialization vector:"); + debugPrint(hc->initialization_vector, 0, 12); + debugD("Additional authenticated data:"); + debugPrint(hc->additional_authenticated_data, 0, 17); + debugD("Authentication tag:"); + debugPrint(hc->authentication_tag, 0, 8); + } + } + if(pos >= 0) { + debugI("Valid HDLC, start at %d", pos); + if(!hw.ledBlink(LED_GREEN, 1)) + hw.ledBlink(LED_INTERNAL, 1); + AmsData data = AmsData(((char *) (buf)) + pos, meterConfig.substituteMissing); + if(data.getListType() > 0) { + if(mqttEnabled && mqttHandler != NULL) { + if(mqttHandler->publish(&data, &meterState)) { + if(data.getListType() == 3 && eapi != NULL) { + mqttHandler->publishPrices(eapi); + } + if(data.getListType() >= 2) { + mqttHandler->publishSystem(&hw); + } + time_t now = time(nullptr); + if(now < EPOCH_2021_01_01 || data.getListType() == 3) { + if(data.getMeterTimestamp() > EPOCH_2021_01_01 || !ntpEnabled) { + debugI("Using timestamp from meter"); + now = data.getMeterTimestamp(); + } else if(data.getPackageTimestamp() > EPOCH_2021_01_01) { + debugI("Using timestamp from meter (DLMS)"); + now = data.getPackageTimestamp(); + } + if(now > EPOCH_2021_01_01) { + timeval tv { now, 0}; + settimeofday(&tv, nullptr); + } + } + } + mqtt.loop(); + delay(10); + } + meterState.apply(data); + } + } else { + debugW("Invalid HDLC, returned with %d", pos); + } + } +} + +void debugPrint(byte *buffer, int start, int length) { + for (int i = start; i < start + length; i++) { + if (buffer[i] < 0x10) + Debug.print("0"); + Debug.print(buffer[i], HEX); + Debug.print(" "); + if ((i - start + 1) % 16 == 0) + Debug.println(""); + else if ((i - start + 1) % 4 == 0) + Debug.print(" "); + + yield(); // Let other get some resources too + } + Debug.println(""); +} + +void oldReadHanPort() { if (hanReader.read()) { lastSuccessfulRead = millis(); delay(1); diff --git a/src/ams/ams.cpp b/src/ams/ams.cpp new file mode 100644 index 00000000..b36aa4f2 --- /dev/null +++ b/src/ams/ams.cpp @@ -0,0 +1,119 @@ +#include "ams.h" +#include +#include "lwip/def.h" +#include "Time.h" + +time_t AMS_getTimestamp(uint8_t* obis, const char* ptr) { + CosemData* item = AMS_findObis(obis, ptr); + if(item != NULL) { + switch(item->base.type) { + case CosemTypeOctetString: { + if(item->oct.length == 0x0C) { + AmsOctetTimestamp* ts = (AmsOctetTimestamp*) item; + tmElements_t tm; + tm.Year = ntohs(ts->year) - 1970; + tm.Month = ts->month; + tm.Day = ts->dayOfMonth; + tm.Hour = ts->hour; + tm.Minute = ts->minute; + tm.Second = ts->second; + + time_t time = makeTime(tm); + int16_t deviation = ntohs(ts->deviation); + if(deviation >= -720 && deviation <= 720) { + time -= deviation * 60; + } + return time; + } + } + } + } + return 0; +} + +uint8_t AMS_getString(uint8_t* obis, const char* ptr, char* target) { + CosemData* item = AMS_findObis(obis, ptr); + if(item != NULL) { + switch(item->base.type) { + case CosemTypeString: + memcpy(target, item->str.data, item->str.length); + target[item->str.length] = 0; + return item->str.length; + case CosemTypeOctetString: + memcpy(target, item->oct.data, item->oct.length); + target[item->oct.length] = 0; + return item->oct.length; + } + } + return 0; +} + +uint32_t AMS_getUnsignedNumber(uint8_t* obis, const char* ptr) { + CosemData* item = AMS_findObis(obis, ptr); + if(item != NULL) { + switch(item->base.type) { + case CosemTypeLongUnsigned: + return ntohs(item->lu.data); + case CosemTypeDLongUnsigned: + return ntohl(item->dlu.data); + } + } + return 0xFFFFFFFF; +} + +int32_t AMS_getSignedNumber(uint8_t* obis, const char* ptr) { + CosemData* item = AMS_findObis(obis, ptr); + if(item != NULL) { + switch(item->base.type) { + case CosemTypeLongUnsigned: + return ntohs(item->lu.data); + case CosemTypeDLongUnsigned: + return ntohl(item->dlu.data); + case CosemTypeLongSigned: + return ntohs(item->lu.data); + } + } + return 0xFFFFFFFF; +} + +CosemData* AMS_findObis(uint8_t* obis, const char* ptr) { + CosemData* item = (CosemData*) ptr; + int ret = 0; + char* pos = (char*) ptr; + do { + item = (CosemData*) pos; + if(ret == 1) return item; + switch(item->base.type) { + case CosemTypeArray: + case CosemTypeStructure: + pos += 2; + break; + case CosemTypeOctetString: { + ret = 1; + uint8_t* found = item->oct.data; + int x = 6 - sizeof(&obis); + for(int i = x; i < 6; i++) { + if(found[i] != obis[i-x]) ret = 0; + } + } + case CosemTypeString: { + pos += 2 + item->base.length; + break; + } + case CosemTypeLongSigned: + pos += 5; + break; + case CosemTypeLongUnsigned: + pos += 3; + break; + case CosemTypeDLongUnsigned: + pos += 5; + break; + case CosemTypeNull: + return NULL; + default: + pos += 2; + } + } while(item->base.type != HDLC_FLAG); + return NULL; +} \ No newline at end of file diff --git a/src/ams/ams.h b/src/ams/ams.h new file mode 100644 index 00000000..343d61a9 --- /dev/null +++ b/src/ams/ams.h @@ -0,0 +1,34 @@ +#ifndef _AMS_H +#define _AMS_H + +#include "Arduino.h" +#include "hdlc.h" + +enum AmsType { + AmsTypeAidon = 0x01, + AmsTypeKaifa = 0x02, + AmsTypeKamstrup = 0x03, + AmsTypeUnknown = 0xFF +}; + +struct AmsOctetTimestamp { + uint16_t year; + uint8_t month; + uint8_t dayOfMonth; + uint8_t dayOfWeek; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t hundredths; + int16_t deviation; + uint8_t status; +} __attribute__((packed)); + + +CosemData* AMS_findObis(uint8_t* obis, const char* ptr); +uint32_t AMS_getUnsignedNumber(uint8_t* obis, const char* ptr); +int32_t AMS_getSignedNumber(uint8_t* obis, const char* ptr); +uint8_t AMS_getString(uint8_t* obis, const char* ptr, char* target); +time_t AMS_getTimestamp(uint8_t* obis, const char* ptr); + +#endif diff --git a/src/ams/crc.cpp b/src/ams/crc.cpp new file mode 100644 index 00000000..cabcac9a --- /dev/null +++ b/src/ams/crc.cpp @@ -0,0 +1,12 @@ +#include "crc.h" + +uint16_t crc16_x25(const uint8_t* p, int len) +{ + uint16_t crc = UINT16_MAX; + + while(len--) + for (uint16_t i = 0, d = 0xff & *p++; i < 8; i++, d >>= 1) + crc = ((crc & 1) ^ (d & 1)) ? (crc >> 1) ^ 0x8408 : (crc >> 1); + + return (~crc << 8) | (~crc >> 8 & 0xff); +} diff --git a/src/ams/crc.h b/src/ams/crc.h new file mode 100644 index 00000000..c4c64616 --- /dev/null +++ b/src/ams/crc.h @@ -0,0 +1,9 @@ +#ifndef _CRC_H +#define _CRC_H + +#include "Arduino.h" +#include + +uint16_t crc16_x25(const uint8_t* p, int len); + +#endif diff --git a/src/ams/hdlc.cpp b/src/ams/hdlc.cpp new file mode 100644 index 00000000..4e3b8694 --- /dev/null +++ b/src/ams/hdlc.cpp @@ -0,0 +1,138 @@ +#include "Arduino.h" +#include "hdlc.h" +#include "crc.h" +#include "lwip/def.h" +#if defined(ESP8266) +#include "bearssl/bearssl.h" +#elif defined(ESP32) +#include "mbedtls/gcm.h" +#endif + +int wtf = 48; + +void mbus_hexdump(const uint8_t* buf, int len) { + printf("\nDUMP (%db) [ ", len); + for(const uint8_t* p = buf; p-buf < len; ++p) + printf("%02X ", *p); + printf("]\n"); +} + +int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config) { + //mbus_hexdump(d, len); + + HDLCHeader* h = (HDLCHeader*) d; + + // Length field (11 lsb of format) + len = (ntohs(h->format) & 0x7FF) + 2; + + HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f); + + // First and last byte should be MBUS_HAN_TAG + if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG) + return -1; + + // Verify FCS + if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1)) + return -2; + + int headersize = 8; + int footersize = 3; + uint8_t* ptr = (uint8_t*) &h[1]; + // Frame format type 3 + if((h->format & 0xF0) == 0xA0) { + + // Skip destination address, LSB marks last byte + while(((*ptr) & 0x01) == 0x00) { + ptr++; + headersize++; + } + ptr++; + + // Skip source address, LSB marks last byte + while(((*ptr) & 0x01) == 0x00) { + ptr++; + headersize++; + } + ptr++; + + HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr); + headersize += 3; + + // Verify HCS + if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d)) + return -3; + + ptr += sizeof *t3; + } + + // Extract LLC + HDLCLLC* llc = (HDLCLLC*) ptr; + ptr += sizeof *llc; + + if(((*ptr) & 0xFF) == 0x0F) { + // Unencrypted APDU + int i = 0; + HDLCADPU* adpu = (HDLCADPU*) (ptr); + ptr += sizeof *adpu; + + // ADPU timestamp + CosemData* dateTime = (CosemData*) ptr; + if(dateTime->base.type == CosemTypeOctetString) + ptr += 2 + dateTime->base.length; + else if(dateTime->base.type == CosemTypeNull) { + ptr++; + } else { + return -99; + } + + return ptr-d; + } else if(((*ptr) & 0xFF) == 0xDB) { + // Encrypted APDU + // http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html + if(config == NULL) + return -90; + + memcpy(config->system_title, d + headersize + 2, 8); + memcpy(config->initialization_vector, config->system_title, 8); + memcpy(config->initialization_vector + 8, d + headersize + 14, 4); + memcpy(config->additional_authenticated_data, d + headersize + 13, 1); + memcpy(config->additional_authenticated_data + 1, config->authentication_key, 16); + memcpy(config->authentication_tag, d + headersize + len - headersize - footersize - 12, 12); + + #if defined(ESP8266) + br_gcm_context gcmCtx; + br_aes_ct_ctr_keys bc; + br_aes_ct_ctr_init(&bc, config->encryption_key, 16); + br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32); + br_gcm_reset(&gcmCtx, config->initialization_vector, sizeof(config->initialization_vector)); + br_gcm_aad_inject(&gcmCtx, config->additional_authenticated_data, sizeof(config->additional_authenticated_data)); + br_gcm_flip(&gcmCtx); + br_gcm_run(&gcmCtx, 0, (void*) (d + headersize + 18), (len - headersize - footersize - 18 - 12)); + if(br_gcm_check_tag_trunc(&gcmCtx, config->authentication_tag, 12) != 1) { + return -91; + } + #elif defined(ESP32) + uint8_t cipher_text[len - headersize - footersize - 18 - 12]; + memcpy(cipher_text, d + headersize + 18, len - headersize - footersize - 12 - 18); + + mbedtls_gcm_context m_ctx; + mbedtls_gcm_init(&m_ctx); + int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, config->encryption_key, 128); + if (0 != success ) { + return -92; + } + success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), initialization_vector, sizeof(initialization_vector), + additional_authenticated_data, sizeof(additional_authenticated_data), authentication_tag, sizeof(authentication_tag), + cipher_text, (unsigned char*)(d + headersize + 18)); + if (0 != success) { + return -91; + } + mbedtls_gcm_free(&m_ctx); + #endif + ptr += 36; // TODO: Come to this number in a proper way... + return ptr-d; + } + + // No payload + return 0; +} diff --git a/src/ams/hdlc.h b/src/ams/hdlc.h new file mode 100644 index 00000000..9f618fe2 --- /dev/null +++ b/src/ams/hdlc.h @@ -0,0 +1,94 @@ +#ifndef _HDLC_H +#define _HDLC_H + +#include "Arduino.h" +#include + +#define HDLC_FLAG 0x7E + +struct HDLCConfig { + uint8_t encryption_key[32]; + uint8_t authentication_key[32]; + uint8_t system_title[8]; + uint8_t initialization_vector[12]; + uint8_t additional_authenticated_data[17]; + uint8_t authentication_tag[12]; +}; + +typedef struct HDLCHeader { + uint8_t flag; + uint16_t format; +} __attribute__((packed)) HDLCHeader; + +typedef struct HDLCFooter { + uint16_t fcs; + uint8_t flag; +} __attribute__((packed)) HDLCFooter; + +typedef struct HDLC3CtrlHcs { + uint8_t control; + uint16_t hcs; +} __attribute__((packed)) HDLC3CtrlHcs; + +typedef struct HDLCLLC { + uint8_t dst; + uint8_t src; + uint8_t control; +} __attribute__((packed)) HDLCLLC; + +typedef struct HDLCADPU { + uint8_t flag; + uint32_t id; +} __attribute__((packed)) HDLCADPU; + +// Blue book, Table 2 +enum CosemType { + CosemTypeNull = 0x00, + CosemTypeArray = 0x01, + CosemTypeStructure = 0x02, + CosemTypeOctetString = 0x09, + CosemTypeString = 0x0A, + CosemTypeDLongUnsigned = 0x06, + CosemTypeLongSigned = 0x10, + CosemTypeLongUnsigned = 0x12 +}; + +struct CosemBasic { + uint8_t type; + uint8_t length; +} __attribute__((packed)); + +struct CosemString { + uint8_t type; + uint8_t length; + uint8_t data[]; +} __attribute__((packed)); + +struct CosemLongUnsigned { + uint8_t type; + uint16_t data; +} __attribute__((packed)); + +struct CosemDLongUnsigned { + uint8_t type; + uint32_t data; +} __attribute__((packed)); + +struct CosemLongSigned { + uint8_t type; + int16_t data; +} __attribute__((packed)); + +typedef union { + struct CosemBasic base; + struct CosemString str; + struct CosemString oct; + struct CosemLongUnsigned lu; + struct CosemDLongUnsigned dlu; + struct CosemLongSigned ls; +} CosemData; + +void mbus_hexdump(const uint8_t* buf, int len); +int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config); + +#endif