Files
P1-wifi-esp12e/P1_gateway_FW/lib/dsmr/fields.h
2021-06-14 08:24:14 +02:00

353 lines
16 KiB
C++

/**
* Arduino DSMR parser.
*
* This software is licensed under the MIT License.
*
* Copyright (c) 2015 Matthijs Kooijman <matthijs@stdin.nl>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Field parsing functions
*/
#ifndef DSMR_INCLUDE_FIELDS_H
#define DSMR_INCLUDE_FIELDS_H
#include "util.h"
#include "parser.h"
namespace dsmr {
/**
* Superclass for data items in a P1 message.
*/
template <typename T>
struct ParsedField {
template <typename F>
void apply(F& f) {
f.apply(*static_cast<T*>(this));
}
// By defaults, fields have no unit
static const char *unit() { return ""; }
};
template <typename T, size_t minlen, size_t maxlen>
struct StringField : ParsedField<T> {
ParseResult<void> parse(const char *str, const char *end) {
ParseResult<String> res = StringParser::parse_string(minlen, maxlen, str, end);
if (!res.err)
static_cast<T*>(this)->val() = res.result;
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
// single integer needs > 4 bytes top fit and isn't very useful (you
// cannot really do any calculation with those values). So we just parse
// into a string for now.
template <typename T>
struct TimestampField : StringField<T, 13, 13> { };
// Value that is parsed as a three-decimal float, but stored as an
// integer (by multiplying by 1000). Supports val() (or implicit cast to
// float) to get the original value, and int_val() to get the more
// efficient integer value. The unit() and int_unit() methods on
// FixedField return the corresponding units for these values.
struct FixedValue {
operator float() { return val();}
float val() { return _value / 1000.0;}
uint32_t int_val() { return _value; }
uint32_t _value;
};
// Floating point numbers in the message never have more than 3 decimal
// digits. To prevent inefficient floating point operations, we store
// them as a fixed-point number: an integer that stores the value in
// thousands. For example, a value of 1.234 kWh is stored as 1234. This
// effectively means that the integer value is het value in Wh. To allow
// automatic printing of these values, both the original unit and the
// integer unit is passed as a template argument.
template <typename T, const char *_unit, const char *_int_unit>
struct FixedField : ParsedField<T> {
ParseResult<void> parse(const char *str, const char *end) {
ParseResult<uint32_t> res = NumParser::parse(3, _unit, str, end);
if (!res.err)
static_cast<T*>(this)->val()._value = res.result;
return res;
}
static const char *unit() { return _unit; }
static const char *int_unit() { return _int_unit; }
};
struct TimestampedFixedValue : public FixedValue {
String timestamp;
};
// Some numerical values are prefixed with a timestamp. This is simply
// both of them concatenated, e.g. 0-1:24.2.1(150117180000W)(00473.789*m3)
template <typename T, const char *_unit, const char *_int_unit>
struct TimestampedFixedField : public FixedField<T, _unit, _int_unit> {
ParseResult<void> parse(const char *str, const char *end) {
// First, parse timestamp
ParseResult<String> res = StringParser::parse_string(13, 13, str, end);
if (res.err)
return res;
static_cast<T*>(this)->val().timestamp = res.result;
// Which is immediately followed by the numerical value
return FixedField<T, _unit, _int_unit>::parse(res.next, end);
}
};
// A integer number is just represented as an integer.
template <typename T, const char *_unit>
struct IntField : ParsedField<T> {
ParseResult<void> parse(const char *str, const char *end) {
ParseResult<uint32_t> res = NumParser::parse(0, _unit, str, end);
if (!res.err)
static_cast<T*>(this)->val() = res.result;
return res;
}
static const char *unit() { return _unit; }
};
// A RawField is not parsed, the entire value (including any
// parenthesis around it) is returned as a string.
template <typename T>
struct RawField : ParsedField<T> {
ParseResult<void> parse(const char *str, const char *end) {
// Just copy the string verbatim value without any parsing
concat_hack(static_cast<T*>(this)->val(), str, end - str);
return ParseResult<void>().until(end);
}
};
namespace fields {
struct units {
// These variables are inside a struct, since that allows us to make
// them constexpr and define their values here, but define the storage
// in a cpp file. Global const(expr) variables have implicitly
// internal linkage, meaning each cpp file that includes us will have
// its own copy of the variable. Since we take the address of these
// variables (passing it as a template argument), this would cause a
// compiler warning. By putting these in a struct, this is prevented.
static constexpr char none[] = "";
static constexpr char kWh[] = "kWh";
static constexpr char Wh[] = "Wh";
static constexpr char kW[] = "kW";
static constexpr char W[] = "W";
static constexpr char V[] = "V";
static constexpr char mV[] = "mV";
static constexpr char A[] = "A";
static constexpr char mA[] = "mA";
static constexpr char m3[] = "m3";
static constexpr char dm3[] = "dm3";
static constexpr char GJ[] = "GJ";
static constexpr char MJ[] = "MJ";
};
const uint8_t GAS_MBUS_ID = 1;
const uint8_t WATER_MBUS_ID = 2;
const uint8_t THERMAL_MBUS_ID = 3;
const uint8_t SLAVE_MBUS_ID = 4;
#define DEFINE_FIELD(fieldname, value_t, obis, field_t, field_args...) \
struct fieldname : field_t<fieldname, ##field_args> { \
value_t fieldname; \
bool fieldname ## _present = false; \
static constexpr ObisId id = obis; \
static constexpr char name_progmem[] DSMR_PROGMEM = #fieldname; \
static constexpr const __FlashStringHelper *name = reinterpret_cast<const __FlashStringHelper*>(&name_progmem); \
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 */
DEFINE_FIELD(identification, String, ObisId(255, 255, 255, 255, 255, 255), RawField);
/* Version information for P1 output */
DEFINE_FIELD(p1_version, String, ObisId(1, 3, 0, 2, 8), StringField, 2, 2);
/* Date-time stamp of the P1 message */
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);
/* Meter Reading electricity delivered to client (Tariff 1) in 0,001 kWh */
DEFINE_FIELD(energy_delivered_tariff1, FixedValue, ObisId(1, 0, 1, 8, 1), FixedField, units::kWh, units::Wh);
/* Meter Reading electricity delivered to client (Tariff 2) in 0,001 kWh */
DEFINE_FIELD(energy_delivered_tariff2, FixedValue, ObisId(1, 0, 1, 8, 2), FixedField, units::kWh, units::Wh);
/* Meter Reading electricity delivered by client (Tariff 1) in 0,001 kWh */
DEFINE_FIELD(energy_returned_tariff1, FixedValue, ObisId(1, 0, 2, 8, 1), FixedField, units::kWh, units::Wh);
/* Meter Reading electricity delivered by client (Tariff 2) in 0,001 kWh */
DEFINE_FIELD(energy_returned_tariff2, FixedValue, ObisId(1, 0, 2, 8, 2), FixedField, units::kWh, units::Wh);
/* Tariff indicator electricity. The tariff indicator can also be used
* to switch tariff dependent loads e.g boilers. This is the
* responsibility of the P1 user */
DEFINE_FIELD(electricity_tariff, String, ObisId(0, 0, 96, 14, 0), StringField, 4, 4);
/* Actual electricity power delivered (+P) in 1 Watt resolution */
DEFINE_FIELD(power_delivered, FixedValue, ObisId(1, 0, 1, 7, 0), FixedField, units::kW, units::W);
/* Actual electricity power received (-P) in 1 Watt resolution */
DEFINE_FIELD(power_returned, FixedValue, ObisId(1, 0, 2, 7, 0), FixedField, units::kW, units::W);
/* The actual threshold Electricity in kW. Removed in 4.0.7 / 4.2.2 / 5.0 */
DEFINE_FIELD(electricity_threshold, FixedValue, ObisId(0, 0, 17, 0, 0), FixedField, units::kW, units::W);
/* Switch position Electricity (in/out/enabled). Removed in 4.0.7 / 4.2.2 / 5.0 */
DEFINE_FIELD(electricity_switch_position, uint8_t, ObisId(0, 0, 96, 3, 10), IntField, units::none);
/* Number of power failures in any phase */
DEFINE_FIELD(electricity_failures, uint32_t, ObisId(0, 0, 96, 7, 21), IntField, units::none);
/* Number of long power failures in any phase */
DEFINE_FIELD(electricity_long_failures, uint32_t, ObisId(0, 0, 96, 7, 9), IntField, units::none);
/* Power Failure Event Log (long power failures) */
DEFINE_FIELD(electricity_failure_log, String, ObisId(1, 0, 99, 97, 0), RawField);
/* Number of voltage sags in phase L1 */
DEFINE_FIELD(electricity_sags_l1, uint32_t, ObisId(1, 0, 32, 32, 0), IntField, units::none);
/* Number of voltage sags in phase L2 (polyphase meters only) */
DEFINE_FIELD(electricity_sags_l2, uint32_t, ObisId(1, 0, 52, 32, 0), IntField, units::none);
/* Number of voltage sags in phase L3 (polyphase meters only) */
DEFINE_FIELD(electricity_sags_l3, uint32_t, ObisId(1, 0, 72, 32, 0), IntField, units::none);
/* Number of voltage swells in phase L1 */
DEFINE_FIELD(electricity_swells_l1, uint32_t, ObisId(1, 0, 32, 36, 0), IntField, units::none);
/* Number of voltage swells in phase L2 (polyphase meters only) */
DEFINE_FIELD(electricity_swells_l2, uint32_t, ObisId(1, 0, 52, 36, 0), IntField, units::none);
/* Number of voltage swells in phase L3 (polyphase meters only) */
DEFINE_FIELD(electricity_swells_l3, uint32_t, ObisId(1, 0, 72, 36, 0), IntField, units::none);
/* Text message codes: numeric 8 digits (Note: Missing from 5.0 spec)
* */
DEFINE_FIELD(message_short, String, ObisId(0, 0, 96, 13, 1), StringField, 0, 16);
/* Text message max 2048 characters (Note: Spec says 1024 in comment and
* 2048 in format spec, so we stick to 2048). */
DEFINE_FIELD(message_long, String, ObisId(0, 0, 96, 13, 0), StringField, 0, 2048);
/* Instantaneous voltage L1 in 0.1V resolution (Note: Spec says V
* resolution in comment, but 0.1V resolution in format spec. Added in
* 5.0) */
DEFINE_FIELD(voltage_l1, FixedValue, ObisId(1, 0, 32, 7, 0), FixedField, units::V, units::mV);
/* Instantaneous voltage L2 in 0.1V resolution (Note: Spec says V
* resolution in comment, but 0.1V resolution in format spec. Added in
* 5.0) */
DEFINE_FIELD(voltage_l2, FixedValue, ObisId(1, 0, 52, 7, 0), FixedField, units::V, units::mV);
/* Instantaneous voltage L3 in 0.1V resolution (Note: Spec says V
* resolution in comment, but 0.1V resolution in format spec. Added in
* 5.0) */
DEFINE_FIELD(voltage_l3, FixedValue, ObisId(1, 0, 72, 7, 0), FixedField, units::V, units::mV);
/* Instantaneous current L1 in A resolution */
DEFINE_FIELD(current_l1, uint16_t, ObisId(1, 0, 31, 7, 0), IntField, units::A);
/* Instantaneous current L2 in A resolution */
DEFINE_FIELD(current_l2, uint16_t, ObisId(1, 0, 51, 7, 0), IntField, units::A);
/* Instantaneous current L3 in A resolution */
DEFINE_FIELD(current_l3, uint16_t, ObisId(1, 0, 71, 7, 0), IntField, units::A);
/* Instantaneous active power L1 (+P) in W resolution */
DEFINE_FIELD(power_delivered_l1, FixedValue, ObisId(1, 0, 21, 7, 0), FixedField, units::kW, units::W);
/* Instantaneous active power L2 (+P) in W resolution */
DEFINE_FIELD(power_delivered_l2, FixedValue, ObisId(1, 0, 41, 7, 0), FixedField, units::kW, units::W);
/* Instantaneous active power L3 (+P) in W resolution */
DEFINE_FIELD(power_delivered_l3, FixedValue, ObisId(1, 0, 61, 7, 0), FixedField, units::kW, units::W);
/* Instantaneous active power L1 (-P) in W resolution */
DEFINE_FIELD(power_returned_l1, FixedValue, ObisId(1, 0, 22, 7, 0), FixedField, units::kW, units::W);
/* Instantaneous active power L2 (-P) in W resolution */
DEFINE_FIELD(power_returned_l2, FixedValue, ObisId(1, 0, 42, 7, 0), FixedField, units::kW, units::W);
/* Instantaneous active power L3 (-P) in W resolution */
DEFINE_FIELD(power_returned_l3, FixedValue, ObisId(1, 0, 62, 7, 0), FixedField, units::kW, units::W);
/* Device-Type */
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);
/* 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);
/* 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);
/* Device-Type */
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);
/* 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);
/* Last 5-minute Meter reading Heat or Cold in 0,01 GJ and capture time
* (Note: 4.x spec has "hourly meter reading") */
DEFINE_FIELD(thermal_delivered, TimestampedFixedValue, ObisId(0, THERMAL_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::GJ, units::MJ);
/* Device-Type */
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);
/* 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);
/* Last 5-minute Meter reading in 0,001 m3 and capture time
* (Note: 4.x spec has "hourly meter reading") */
DEFINE_FIELD(water_delivered, TimestampedFixedValue, ObisId(0, WATER_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3);
/* Device-Type */
DEFINE_FIELD(slave_device_type, uint16_t, ObisId(0, SLAVE_MBUS_ID, 24, 1, 0), IntField, units::none);
/* Equipment identifier (Thermal: heat or cold) */
DEFINE_FIELD(slave_equipment_id, String, ObisId(0, SLAVE_MBUS_ID, 96, 1, 0), StringField, 0, 96);
/* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */
DEFINE_FIELD(slave_valve_position, uint8_t, ObisId(0, SLAVE_MBUS_ID, 24, 4, 0), IntField, units::none);
/* Last 5-minute Meter reading Heat or Cold and capture time (e.g. slave
* E meter) (Note: 4.x spec has "hourly meter reading") */
DEFINE_FIELD(slave_delivered, TimestampedFixedValue, ObisId(0, SLAVE_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3);
} // namespace fields
} // namespace dsmr
#endif // DSMR_INCLUDE_FIELDS_H