From 776e086fb9ec055761eac18d988057ef31b8f16c Mon Sep 17 00:00:00 2001 From: Pauline Middelink Date: Wed, 9 Aug 2023 15:45:47 +0200 Subject: [PATCH] Add a HexStringField parser which understands some DSMRs encode the equipement ids as hex strings. --- README.md | 12 +++++++++++- src/dsmr/fields.h | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index df7278b..c9f6c24 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,8 @@ need, to make the parsing and printing code smaller and faster. Some values are parsed to an Arduino `String` value or C++ integer type, those should be fairly straightforward. There are three special types -that need some explanation: `FixedValue` and `TimestampedFixedValue`. +that need some explanation: `FixedValue`, `TimestampedFixedValue` and +`HexStringField`. When looking at the DSMR P1 format, it defines a floating point format. It is described as `Fn(x,y)`, where `n` is the total number of (decimal) @@ -201,6 +202,15 @@ Parsing these into something like a UNIX timestamp is tricky (think leap years and seconds) and of limited use, so this just keeps the original format. +The final one `HexStringField` is a smart version of the StringField +parser and is only used for equipment id's as in practise these are +often hex coded strings instead of a normal strings. If the given +string is indeed a hex coding, the decoded string is returned else +the original string is returned. + + 0-0:96.1.1(4530303639303030373138323035333231) + E0069000718205321 + ## Connecting the P1 port The P1 port essentially consists of three parts: diff --git a/src/dsmr/fields.h b/src/dsmr/fields.h index 7706536..6b3fd74 100644 --- a/src/dsmr/fields.h +++ b/src/dsmr/fields.h @@ -73,6 +73,41 @@ namespace dsmr } }; + // A hexstring field is essencially a string filled with hex digits. If the + // string does not consist of an even number of hex digits, the original + // string is returned, otherwise the decoded hex string is returned. + template + struct HexStringField : ParsedField + { + ParseResult parse(const char *str, const char *end) + { + ParseResult res = StringParser::parse_string(minlen, maxlen, str, end); + if (!res.err) { + static_cast(this)->val() = res.result; + + if (res.result.length() & 1) { + // Odd number of chars, can't be a hex coded string. + return res; + } + if (!std::all_of(res.result.begin(), res.result.end(), [](const char ch){ return isxdigit(ch); })) { + // Not all chars are hex digits. + return res; + } + String hexStr; + hexStr.reserve(res.result.length()/2); + for (auto it = res.result.begin(); it != res.result.end(); ++it) { + const unsigned char ch1 = static_cast(*it++); + const unsigned char ch2 = static_cast(*it); + uint8_t val = (isdigit(ch1) ? ch1 - '0' : toupper(ch1) - 'A' + 10) * 16 + + (isdigit(ch2) ? ch2 - '0' : toupper(ch2) - 'A' + 10); + hexStr.concat(static_cast(val)); + } + static_cast(this)->val() = hexStr; + } + return res; + } + }; + // A timestamp is essentially a string using YYMMDDhhmmssX format (where // X is W or S for wintertime or summertime). Parsing this into a proper // (UNIX) timestamp is hard to do generically. Parsing it into a @@ -275,7 +310,7 @@ namespace dsmr DEFINE_FIELD(timestamp, String, ObisId(0, 0, 1, 0, 0), TimestampField); /* Equipment identifier */ - DEFINE_FIELD(equipment_id, String, ObisId(0, 0, 96, 1, 1), StringField, 0, 96); + DEFINE_FIELD(equipment_id, String, ObisId(0, 0, 96, 1, 1), HexStringField, 0, 96); /* Meter Reading electricity delivered to client (Special for Lux) in 0,001 kWh */ /* TODO: by OBIS 1-0:1.8.0.255 IEC 62056 it should be Positive active energy (A+) total [kWh], should we rename it? */ @@ -495,9 +530,9 @@ namespace dsmr DEFINE_FIELD(gas_device_type, uint16_t, ObisId(0, GAS_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Gas) */ - DEFINE_FIELD(gas_equipment_id, String, ObisId(0, GAS_MBUS_ID, 96, 1, 0), StringField, 0, 96); + DEFINE_FIELD(gas_equipment_id, String, ObisId(0, GAS_MBUS_ID, 96, 1, 0), HexStringField, 0, 96); /* Equipment identifier (Gas) BE */ - DEFINE_FIELD(gas_equipment_id_be, String, ObisId(0, GAS_MBUS_ID, 96, 1, 1), StringField, 0, 96); + DEFINE_FIELD(gas_equipment_id_be, String, ObisId(0, GAS_MBUS_ID, 96, 1, 1), HexStringField, 0, 96); /* Valve position Gas (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(gas_valve_position, uint8_t, ObisId(0, GAS_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -516,7 +551,7 @@ namespace dsmr DEFINE_FIELD(thermal_device_type, uint16_t, ObisId(0, THERMAL_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ - DEFINE_FIELD(thermal_equipment_id, String, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), StringField, 0, 96); + DEFINE_FIELD(thermal_equipment_id, String, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), HexStringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(thermal_valve_position, uint8_t, ObisId(0, THERMAL_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -530,7 +565,7 @@ namespace dsmr DEFINE_FIELD(water_device_type, uint16_t, ObisId(0, WATER_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ - DEFINE_FIELD(water_equipment_id, String, ObisId(0, WATER_MBUS_ID, 96, 1, 0), StringField, 0, 96); + DEFINE_FIELD(water_equipment_id, String, ObisId(0, WATER_MBUS_ID, 96, 1, 0), HexStringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(water_valve_position, uint8_t, ObisId(0, WATER_MBUS_ID, 24, 4, 0), IntField, units::none); @@ -544,7 +579,7 @@ namespace dsmr DEFINE_FIELD(sub_device_type, uint16_t, ObisId(0, SUB_MBUS_ID, 24, 1, 0), IntField, units::none); /* Equipment identifier (Thermal: heat or cold) */ - DEFINE_FIELD(sub_equipment_id, String, ObisId(0, SUB_MBUS_ID, 96, 1, 0), StringField, 0, 96); + DEFINE_FIELD(sub_equipment_id, String, ObisId(0, SUB_MBUS_ID, 96, 1, 0), HexStringField, 0, 96); /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ DEFINE_FIELD(sub_valve_position, uint8_t, ObisId(0, SUB_MBUS_ID, 24, 4, 0), IntField, units::none);