Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ include(FetchContent)
file(DOWNLOAD
https://github.com/doctest/doctest/releases/download/v2.4.12/doctest.h
${CMAKE_BINARY_DIR}/doctest/doctest.h
EXPECTED_MD5 0671bbca9fb00cb19a168cffa89ac184)
EXPECTED_HASH SHA512=5a69cc6a5e874691877fbd1cea661ec736c2d78d38791cb22a12398c2731ea6d07e7edda4bd1efde2a65df9e4ddd4836f6571d9b04e0241d520790573634065a)

# mbedtls
FetchContent_Populate(
mded_tls
URL https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.4/mbedtls-3.6.4.tar.bz2
URL_HASH MD5=eb965a5bb8044bc43a49adb435fa72ee)
URL_HASH SHA512=6671fb8fcaa832e5b115dfdce8f78baa6a4aea71f5c89a640583634cdee27aefe3bf4be075744da91f7c3ae5ea4e0c765c8fc3937b5cfd9ea73d87ef496524da)
set(ENABLE_PROGRAMS OFF CACHE INTERNAL "")
set(ENABLE_TESTING OFF CACHE INTERNAL "")
add_subdirectory(${mded_tls_SOURCE_DIR})
Expand All @@ -26,16 +26,16 @@ add_subdirectory(${mded_tls_SOURCE_DIR})
FetchContent_Populate(
bearsll
URL https://bearssl.org/bearssl-0.6.tar.gz
URL_HASH MD5=1513f9828c5b174ea409ca581cb45c98)
URL_HASH SHA512=f9ed25683cfc6c4abe7f1203a2b82ed101ee4c9e0f9ab60755b6a09c8d1e8e4f64d413624e7bb9c4b0033f909a2e4568a1d916cc6ce4736222900691e1f8359a)
file(GLOB_RECURSE bearssl_sources CONFIGURE_DEPENDS "${bearsll_SOURCE_DIR}/src/*.c")
add_library(bearssl STATIC ${bearssl_sources})
target_include_directories(bearssl SYSTEM PUBLIC "${bearsll_SOURCE_DIR}/inc" PRIVATE "${bearsll_SOURCE_DIR}/src")

# cmake_template for warnings and sanitizers
FetchContent_Populate(
cmake_template
URL https://github.com/cpp-best-practices/cmake_template/archive/refs/heads/main.zip
URL_HASH MD5=c391b4c21eeabac1d5142bcf404c740c)
URL https://github.com/cpp-best-practices/cmake_template/archive/1015c6b88410df411c0cc0413e3e64c33d7a8331.zip
URL_HASH SHA512=b7ad5b05eb21e5cb8159f3bd8b05615ee55489e5ab6a543bfe50ba05c8c8935106a6870b438c97ca3894108650a4a542a810707954cbe6b3beb9c6de82f1bde7)
include(${cmake_template_SOURCE_DIR}/cmake/CompilerWarnings.cmake)
include(${cmake_template_SOURCE_DIR}/cmake/Sanitizers.cmake)

Expand Down
49 changes: 19 additions & 30 deletions src/dsmr_parser/fields.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,23 +71,13 @@ struct FixedValue {
template <typename T, const char* _unit, const char* _int_unit>
struct FixedField : ParsedField<T> {
ParseResult<void> parse(const char* str, const char* end) {
// Check if the value is a float value, plus its expected unit type.
ParseResult<uint32_t> res_float = NumParser::parse(3, _unit, str, end);
if (!res_float.err) {
static_cast<T*>(this)->val()._value = res_float.result;
return res_float;
}
// If not, then check for an int value, plus its expected unit type.
// This accomodates for some smart meters that publish int values instead
// of floats. E.g. most meters would publish "1-0:1.8.0(000441.879*kWh)",
// Some smart meters publish int values instead of a float.
// E.g. most meters would publish "1-0:1.8.0(000441.879*kWh)",
// but some use "1-0:1.8.0(000441879*Wh)" instead.
ParseResult<uint32_t> res_int = NumParser::parse(0, _int_unit, str, end);
if (!res_int.err) {
static_cast<T*>(this)->val()._value = res_int.result;
return res_int;
}
// If not, then return the initial error result for the float parsing step.
return res_float;
auto res = NumParser::parse_float_or_int(3, _unit, _int_unit, str, end);
if (!res.err)
static_cast<T*>(this)->val()._value = res.result;
return res;
}

static const char* unit() noexcept { return _unit; }
Expand Down Expand Up @@ -199,12 +189,9 @@ struct AveragedFixedField : public FixedField<T, _unit, _int_unit> {
return res;

// parse value (04.329*kW) or (04329*W)
auto monthValue = NumParser::parse(3, _unit, res.next, end);
if (monthValue.err) {
monthValue = NumParser::parse(0, _int_unit, res.next, end);
if (monthValue.err)
return monthValue;
}
auto monthValue = NumParser::parse_float_or_int(3, _unit, _int_unit, res.next, end);
if (monthValue.err)
return monthValue;

average.next = monthValue.next;
average.result += monthValue.result;
Expand Down Expand Up @@ -260,14 +247,14 @@ const uint8_t WATER_MBUS_ID = DSMR_WATER_MBUS_ID;
const uint8_t THERMAL_MBUS_ID = DSMR_THERMAL_MBUS_ID;
const uint8_t SUB_MBUS_ID = DSMR_SUB_MBUS_ID;

#define DEFINE_FIELD(fieldname, value_t, obis, field_t, ...) \
struct fieldname : field_t<fieldname __VA_OPT__(, __VA_ARGS__)> { \
value_t fieldname; \
bool fieldname##_present = false; \
static inline constexpr ObisId id = obis; \
static inline constexpr char name[] = #fieldname; \
value_t& val() { return fieldname; } \
bool& present() { return fieldname##_present; } \
#define DEFINE_FIELD(fieldname, value_t, obis, field_t, ...) \
struct fieldname : field_t<fieldname __VA_OPT__(, __VA_ARGS__)> { \
value_t fieldname; \
bool fieldname##_present = false; \
static inline constexpr ObisId id = obis; \
static inline constexpr char name[] = #fieldname; \
value_t& val() { return fieldname; } \
bool& present() { return fieldname##_present; } \
}

// Meter identification. This is not a normal field, but a specially-formatted first line of the message
Expand Down Expand Up @@ -514,6 +501,8 @@ DEFINE_FIELD(gas_valve_position, uint8_t, ObisId(0, GAS_MBUS_ID, 24, 4, 0), IntF
// Last 5-minute value (temperature converted), gas delivered to client
// in m3, including decimal values and capture time (Note: 4.x spec has "hourly value")
DEFINE_FIELD(gas_delivered, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3);
// Eneco in the Netherlands has smart meters for their district heating network, which uses the gas_delivered in GJ rather than m3
DEFINE_FIELD(gas_delivered_gj, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::GJ, units::MJ);
// _BE
DEFINE_FIELD(gas_delivered_be, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 3), TimestampedFixedField, units::m3, units::dm3);
DEFINE_FIELD(gas_delivered_text, std::string, ObisId(0, GAS_MBUS_ID, 24, 3, 0), RawField);
Expand Down
12 changes: 12 additions & 0 deletions src/dsmr_parser/parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ static constexpr char INVALID_NUMBER[] = "Invalid number";
static constexpr char INVALID_UNIT[] = "Invalid unit";

struct NumParser final {
static ParseResult<uint32_t> parse_float_or_int(const size_t max_decimals, const char* float_unit, const char* int_unit, const char* str, const char* end) {
auto float_res = NumParser::parse(max_decimals, float_unit, str, end);
if (!float_res.err)
return float_res;

auto int_res = NumParser::parse(0, int_unit, str, end);
if (!int_res.err)
return int_res;

return float_res;
}

static ParseResult<uint32_t> parse(size_t max_decimals, const char* unit, const char* str, const char* end) {
ParseResult<uint32_t> res;
if (str >= end || *str != '(')
Expand Down
16 changes: 14 additions & 2 deletions tests/parser_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ TEST_CASE("Whitespace after OBIS ID") {
"0-1:24.2.1 (000101000000W)(00000000.0000)\r\n"
"!";
ParsedData<gas_delivered> data;
const auto& res = P1Parser::parse(data, msg, std::size(msg), /*unknown_error=*/false, /*check_crc=*/false);
const auto& res = P1Parser::parse(data, msg, std::size(msg), /*unknown_error=*/ true, /*check_crc=*/false);
REQUIRE(std::string(res.err) == "Missing (");
}

Expand All @@ -624,7 +624,7 @@ TEST_CASE("Use integer fallback unit") {
"1-0:14.7.0(50*Hz)\r\n"
"!";
ParsedData<gas_delivered, frequency> data;
P1Parser::parse(data, msg, std::size(msg), /*unknown_error=*/false, /*check_crc=*/false);
P1Parser::parse(data, msg, std::size(msg), /*unknown_error=*/ true, /*check_crc=*/false);
REQUIRE(data.gas_delivered == 0.012f);
REQUIRE(data.frequency == 0.05f);
}
Expand Down Expand Up @@ -655,3 +655,15 @@ TEST_CASE("AveragedFixedField works properly for an empty array") {
REQUIRE(data.active_energy_import_maximum_demand_last_13_months.val() == 0.0f);
REQUIRE(data.energy_delivered_tariff1.val() == 1.0f);
}

TEST_CASE("Should parse gas_delivered_gj field") {
const auto& msg = "/identification\r\n"
"0-1:24.2.1(251129203200W)(3.829*GJ)\r\n"
"!";

ParsedData<gas_delivered_gj> data;

const auto& res = P1Parser::parse(data, msg, std::size(msg), /* unknown_error */ true, /* check_crc */ false);
REQUIRE(res.err == nullptr);
REQUIRE(data.gas_delivered_gj == 3.829f);
}