/** * Arduino DSMR parser. * * This software is licensed under the MIT License. * * Copyright (c) 2015 Matthijs Kooijman * * 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 struct ParsedField { template void apply(F& f) { f.apply(*static_cast(this)); } // By defaults, fields have no unit static const char *unit() { return ""; } }; template struct StringField : 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; 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 struct TimestampField : StringField { }; // 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 struct FixedField : ParsedField { ParseResult parse(const char *str, const char *end) { ParseResult res = NumParser::parse(3, _unit, str, end); if (!res.err) static_cast(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 struct TimestampedFixedField : public FixedField { ParseResult parse(const char *str, const char *end) { // First, parse timestamp ParseResult res = StringParser::parse_string(13, 13, str, end); if (res.err) return res; static_cast(this)->val().timestamp = res.result; // Which is immediately followed by the numerical value return FixedField::parse(res.next, end); } }; // A integer number is just represented as an integer. template struct IntField : ParsedField { ParseResult parse(const char *str, const char *end) { ParseResult res = NumParser::parse(0, _unit, str, end); if (!res.err) static_cast(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 struct RawField : ParsedField { ParseResult parse(const char *str, const char *end) { // Just copy the string verbatim value without any parsing concat_hack(static_cast(this)->val(), str, end - str); return ParseResult().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 { \ 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(&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