From 00def1d8d1a74e4ecd7b49faec8c029293cd134a Mon Sep 17 00:00:00 2001 From: qubeck Date: Fri, 7 Apr 2023 20:20:00 +0200 Subject: [PATCH] Generic SML based power meters support (#146) * add support for energy & power readings on SML based power meters, taking OBIS 16.7.1 for power (using mod. SML Parser lib. by olliiiver) * switched SML read to use software serial * made total power meter response controled by meter source to obtain either the sum of phase powers or explicit total power provided by meter * made mqtt subscriptions to power meter topics meter source dependend * simplified SML read loop and OBIS handler registration, + minor refactoring * minor cleanup/style changes and optim. PowerMeter * fixed build, add SOURCE_SML == 4 * removed optional usage of HW serial for SML power meter * switched to usage of _powerMeter1Power for SML power reading to allign better with existing code --------- Co-authored-by: helgeerbe --- include/PowerMeter.h | 24 +- lib/SMLParser/sml.cpp | 405 +++++++++++++++++++++++ lib/SMLParser/sml.h | 106 ++++++ lib/SMLParser/smlCrcTable.h | 42 +++ platformio.ini | 1 + src/PowerMeter.cpp | 70 +++- webapp/src/locales/de.json | 1 + webapp/src/locales/en.json | 1 + webapp/src/views/PowerMeterAdminView.vue | 3 +- 9 files changed, 637 insertions(+), 16 deletions(-) create mode 100644 lib/SMLParser/sml.cpp create mode 100644 lib/SMLParser/sml.h create mode 100644 lib/SMLParser/smlCrcTable.h diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 8b1ff3355..c7a106989 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -7,6 +7,7 @@ #include #include #include "SDM.h" +#include "sml.h" #ifndef SDM_RX_PIN #define SDM_RX_PIN 13 @@ -16,6 +17,16 @@ #define SDM_TX_PIN 32 #endif +#ifndef SML_RX_PIN +#define SML_RX_PIN 35 +#endif + +typedef struct { + const unsigned char OBIS[6]; + void (*Fn)(double&); + float* Arg; +} OBISHandler; + class PowerMeterClass { public: enum SOURCE { @@ -23,6 +34,7 @@ class PowerMeterClass { SOURCE_SDM1PH = 1, SOURCE_SDM3PH = 2, SOURCE_HTTP = 3, + SOURCE_SML = 4 }; void init(); void mqtt(); @@ -40,14 +52,20 @@ class PowerMeterClass { float _powerMeter1Power = 0.0; float _powerMeter2Power = 0.0; float _powerMeter3Power = 0.0; - float _powerMeterTotalPower = 0.0; float _powerMeter1Voltage = 0.0; float _powerMeter2Voltage = 0.0; float _powerMeter3Voltage = 0.0; - float _PowerMeterImport = 0.0; - float _PowerMeterExport = 0.0; + float _powerMeterImport = 0.0; + float _powerMeterExport = 0.0; bool mqttInitDone = false; + + bool smlReadLoop(); + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} + }; }; extern PowerMeterClass PowerMeter; diff --git a/lib/SMLParser/sml.cpp b/lib/SMLParser/sml.cpp new file mode 100644 index 000000000..7a378f639 --- /dev/null +++ b/lib/SMLParser/sml.cpp @@ -0,0 +1,405 @@ +#include +#include + +#include "sml.h" +#include "smlCrcTable.h" + +#ifdef SML_DEBUG +char logBuff[200]; + +#ifdef SML_NATIVE +#define SML_LOG(...) \ + do { \ + printf(__VA_ARGS__); \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + printf("%.*s", level, " "); \ + printf(__VA_ARGS__); \ + } while (0) +#elif ARDUINO +#include +#define SML_LOG(...) \ + do { \ + sprintf(logBuff, __VA_ARGS__); \ + Serial.print(logBuff); \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + sprintf(logBuff, __VA_ARGS__); \ + Serial.print(logBuff); \ + } while (0) +#endif + +#else +#define SML_LOG(...) \ + do { \ + } while (0) +#define SML_TREELOG(level, ...) \ + do { \ + } while (0) +#endif + +#define MAX_LIST_SIZE 80 +#define MAX_TREE_SIZE 10 + +static sml_states_t currentState = SML_START; +static char nodes[MAX_TREE_SIZE]; +static unsigned char currentLevel = 0; +static unsigned short crc = 0xFFFF; +static signed char sc; +static unsigned short crcMine = 0xFFFF; +static unsigned short crcReceived = 0x0000; +static unsigned char len = 4; +static unsigned char listBuffer[MAX_LIST_SIZE]; /* keeps a list + as length + state + data */ +static unsigned char listPos = 0; + +void crc16(unsigned char &byte) +{ +#ifdef ARDUINO + crc = + pgm_read_word_near(&smlCrcTable[(byte ^ crc) & 0xff]) ^ (crc >> 8 & 0xff); +#else + crc = smlCrcTable[(byte ^ crc) & 0xff] ^ (crc >> 8 & 0xff); +#endif +} + +void setState(sml_states_t state, int byteLen) +{ + currentState = state; + len = byteLen; +} + +void pushListBuffer(unsigned char byte) +{ + if (listPos < MAX_LIST_SIZE) { + listBuffer[listPos++] = byte; + } +} + +void reduceList() +{ + if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0) + nodes[currentLevel]--; +} + +void smlNewList(unsigned char size) +{ + reduceList(); + if (currentLevel < MAX_TREE_SIZE) + currentLevel++; + nodes[currentLevel] = size; + SML_TREELOG(currentLevel, "LISTSTART on level %i with %i nodes\n", + currentLevel, size); + setState(SML_LISTSTART, size); + // @todo workaround for lists inside obis lists + if (size > 5) { + listPos = 0; + memset(listBuffer, '\0', MAX_LIST_SIZE); + } + else { + pushListBuffer(size); + pushListBuffer(currentState); + } +} + +void checkMagicByte(unsigned char &byte) +{ + unsigned int size = 0; + while (currentLevel > 0 && nodes[currentLevel] == 0) { + /* go back in tree if no nodes remaining */ + SML_TREELOG(currentLevel, "back to previous list\n"); + currentLevel--; + } + if (byte > 0x70 && byte <= 0x7F) { + /* new list */ + size = byte & 0x0F; + smlNewList(size); + } + else if (byte >= 0x01 && byte <= 0x6F && nodes[currentLevel] > 0) { + if (byte == 0x01) { + /* no data, get next */ + SML_TREELOG(currentLevel, " Data %i (empty)\n", nodes[currentLevel]); + pushListBuffer(0); + pushListBuffer(currentState); + if (nodes[currentLevel] == 1) { + setState(SML_LISTEND, 1); + SML_TREELOG(currentLevel, "LISTEND\n"); + } + else { + setState(SML_NEXT, 1); + } + } + else { + size = (byte & 0x0F) - 1; + setState(SML_DATA, size); + if ((byte & 0xF0) == 0x50) { + setState(SML_DATA_SIGNED_INT, size); + } + else if ((byte & 0xF0) == 0x60) { + setState(SML_DATA_UNSIGNED_INT, size); + } + else if ((byte & 0xF0) == 0x00) { + setState(SML_DATA_OCTET_STRING, size); + } + SML_TREELOG(currentLevel, + " Data %i (length = %i%s): ", nodes[currentLevel], size, + (currentState == SML_DATA_SIGNED_INT) ? ", signed int" + : (currentState == SML_DATA_UNSIGNED_INT) ? ", unsigned int" + : (currentState == SML_DATA_OCTET_STRING) ? ", octet string" + : ""); + pushListBuffer(size); + pushListBuffer(currentState); + } + reduceList(); + } + else if (byte == 0x00) { + /* end of block */ + reduceList(); + SML_TREELOG(currentLevel, "End of block at level %i\n", currentLevel); + if (currentLevel == 0) { + setState(SML_NEXT, 1); + } + else { + setState(SML_BLOCKEND, 1); + } + } + else if (byte & 0x80) { + // MSB bit is set, another TL byte will follow + if (byte >= 0x80 && byte <= 0x8F) { + // Datatype Octet String + setState(SML_HDATA, (byte & 0x0F) << 4); + } + else if (byte >= 0xF0 /*&& byte <= 0xFF*/) { + /* Datatype List of ...*/ + setState(SML_LISTEXTENDED, (byte & 0x0F) << 4); + } + } + else if (byte == 0x1B && currentLevel == 0) { + /* end sequence */ + setState(SML_END, 3); + } + else { + /* Unexpected Byte */ + SML_TREELOG(currentLevel, + "UNEXPECTED magicbyte >%02X< at currentLevel %i\n", byte, + currentLevel); + setState(SML_UNEXPECTED, 4); + } +} + +sml_states_t smlState(unsigned char ¤tByte) +{ + unsigned char size; + if (len > 0) + len--; + crc16(currentByte); + switch (currentState) { + case SML_UNEXPECTED: + case SML_CHECKSUM_ERROR: + case SML_FINAL: + case SML_START: + currentState = SML_START; + currentLevel = 0; // Reset current level at the begin of a new transmission + // to prevent problems + if (currentByte != 0x1b) + setState(SML_UNEXPECTED, 4); + if (len == 0) { + SML_TREELOG(0, "START\n"); + /* completely clean any garbage from crc checksum */ + crc = 0xFFFF; + currentByte = 0x1b; + crc16(currentByte); + crc16(currentByte); + crc16(currentByte); + crc16(currentByte); + setState(SML_VERSION, 4); + } + break; + case SML_VERSION: + if (currentByte != 0x01) + setState(SML_UNEXPECTED, 4); + if (len == 0) { + setState(SML_BLOCKSTART, 1); + } + break; + case SML_END: + if (currentByte != 0x1b) { + SML_LOG("UNEXPECTED char >%02X< at SML_END\n", currentByte); + setState(SML_UNEXPECTED, 4); + } + if (len == 0) { + setState(SML_CHECKSUM, 4); + } + break; + case SML_CHECKSUM: + // SML_LOG("CHECK: %02X\n", currentByte); + if (len == 2) { + crcMine = crc ^ 0xFFFF; + } + if (len == 1) { + crcReceived += currentByte; + } + if (len == 0) { + crcReceived = crcReceived | (currentByte << 8); + SML_LOG("Received checksum: %02X\n", crcReceived); + SML_LOG("Calculated checksum: %02X\n", crcMine); + if (crcMine == crcReceived) { + setState(SML_FINAL, 4); + } + else { + setState(SML_CHECKSUM_ERROR, 4); + } + crc = 0xFFFF; + crcReceived = 0x000; /* reset CRC */ + } + break; + case SML_HDATA: + size = len + currentByte - 1; + setState(SML_DATA, size); + pushListBuffer(size); + pushListBuffer(currentState); + SML_TREELOG(currentLevel, " Data (length = %i): ", size); + break; + case SML_LISTEXTENDED: + size = len + (currentByte & 0x0F); + SML_TREELOG(currentLevel, "Extended List with Size=%i\n", size); + smlNewList(size); + break; + case SML_DATA: + case SML_DATA_SIGNED_INT: + case SML_DATA_UNSIGNED_INT: + case SML_DATA_OCTET_STRING: + SML_LOG("%02X ", currentByte); + pushListBuffer(currentByte); + if (nodes[currentLevel] == 0 && len == 0) { + SML_LOG("\n"); + SML_TREELOG(currentLevel, "LISTEND on level %i\n", currentLevel); + currentState = SML_LISTEND; + } + else if (len == 0) { + currentState = SML_DATAEND; + SML_LOG("\n"); + } + break; + case SML_DATAEND: + case SML_NEXT: + case SML_LISTSTART: + case SML_LISTEND: + case SML_BLOCKSTART: + case SML_BLOCKEND: + checkMagicByte(currentByte); + break; + } + return currentState; +} + +bool smlOBISCheck(const unsigned char *obis) +{ + return (memcmp(obis, &listBuffer[2], 6) == 0); +} + +void smlOBISManufacturer(unsigned char *str, int maxSize) +{ + int i = 0, pos = 0, size = 0; + while (i < listPos) { + size = (int)listBuffer[i]; + i++; + pos++; + if (pos == 6) { + /* get manufacturer at position 6 in list */ + size = (size > maxSize - 1) ? maxSize : size; + memcpy(str, &listBuffer[i + 1], size); + str[size + 1] = 0; + } + i += size + 1; + } +} + +void smlPow(double &val, signed char &scaler) +{ + if (scaler < 0) { + while (scaler++) { + val /= 10; + } + } + else { + while (scaler--) { + val *= 10; + } + } +} + +void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit) +{ + unsigned char i = 0, pos = 0, size = 0, y = 0, skip = 0; + sml_states_t type; + val = -1; /* unknown or error */ + while (i < listPos) { + pos++; + size = (int)listBuffer[i++]; + type = (sml_states_t)listBuffer[i++]; + if (type == SML_LISTSTART && size > 0) { + // skip a list inside an obis list + skip = size; + while (skip > 0) { + size = (int)listBuffer[i++]; + type = (sml_states_t)listBuffer[i++]; + i += size; + skip--; + } + size = 0; + } + if (pos == 4 && listBuffer[i] != unit) { + /* return unknown (-1) if unit does not match */ + return; + } + if (pos == 5) { + scaler = listBuffer[i]; + } + if (pos == 6) { + y = size; + // initialize 64bit signed integer based on MSB from received value + val = + (type == SML_DATA_SIGNED_INT && (listBuffer[i] & (1 << 7))) ? ~0 : 0; + for (y = 0; y < size; y++) { + // left shift received bytes to 64 bit signed integer + val = (val << 8) | listBuffer[i + y]; + } + } + i += size; + } +} + +void smlOBISWh(double &wh) +{ + long long int val; + smlOBISByUnit(val, sc, SML_WATT_HOUR); + wh = val; + smlPow(wh, sc); +} + +void smlOBISW(double &w) +{ + long long int val; + smlOBISByUnit(val, sc, SML_WATT); + w = val; + smlPow(w, sc); +} + +void smlOBISVolt(double &v) +{ + long long int val; + smlOBISByUnit(val, sc, SML_VOLT); + v = val; + smlPow(v, sc); +} + +void smlOBISAmpere(double &a) +{ + long long int val; + smlOBISByUnit(val, sc, SML_AMPERE); + a = val; + smlPow(a, sc); +} diff --git a/lib/SMLParser/sml.h b/lib/SMLParser/sml.h new file mode 100644 index 000000000..ac6405dff --- /dev/null +++ b/lib/SMLParser/sml.h @@ -0,0 +1,106 @@ +#ifndef SML_H +#define SML_H + +#include + +typedef enum { + SML_START, + SML_END, + SML_VERSION, + SML_NEXT, + SML_LISTSTART, + SML_LISTEND, + SML_LISTEXTENDED, + SML_DATA, + SML_HDATA, + SML_DATAEND, + SML_BLOCKSTART, + SML_BLOCKEND, + SML_CHECKSUM, + SML_CHECKSUM_ERROR, /* calculated checksum does not match */ + SML_UNEXPECTED, /* unexpected byte received */ + SML_FINAL, /* final state, checksum OK */ + SML_DATA_SIGNED_INT, + SML_DATA_UNSIGNED_INT, + SML_DATA_OCTET_STRING, +} sml_states_t; + +typedef enum { + SML_YEAR = 1, + SML_MONTH = 2, + SML_WEEK = 3, + SML_DAY = 4, + SML_HOUR = 5, + SML_MIN = 6, + SML_SECOND = 7, + SML_DEGREE = 8, + SML_DEGREE_CELSIUS = 9, + SML_CURRENCY = 10, + SML_METRE = 11, + SML_METRE_PER_SECOND = 12, + SML_CUBIC_METRE = 13, + SML_CUBIC_METRE_CORRECTED = 14, + SML_CUBIC_METRE_PER_HOUR = 15, + SML_CUBIC_METRE_PER_HOUR_CORRECTED = 16, + SML_CUBIC_METRE_PER_DAY = 17, + SML_CUBIC_METRE_PER_DAY_CORRECTED = 18, + SML_LITRE = 19, + SML_KILOGRAM = 20, + SML_NEWTON = 21, + SML_NEWTONMETER = 22, + SML_PASCAL = 23, + SML_BAR = 24, + SML_JOULE = 25, + SML_JOULE_PER_HOUR = 26, + SML_WATT = 27, + SML_VOLT_AMPERE = 28, + SML_VAR = 29, + SML_WATT_HOUR = 30, + SML_VOLT_AMPERE_HOUR = 31, + SML_VAR_HOUR = 32, + SML_AMPERE = 33, + SML_COULOMB = 34, + SML_VOLT = 35, + SML_VOLT_PER_METRE = 36, + SML_FARAD = 37, + SML_OHM = 38, + SML_OHM_METRE = 39, + SML_WEBER = 40, + SML_TESLA = 41, + SML_AMPERE_PER_METRE = 42, + SML_HENRY = 43, + SML_HERTZ = 44, + SML_ACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 45, + SML_REACTIVE_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 46, + SML_APPARENT_ENERGY_METER_CONSTANT_OR_PULSE_VALUE = 47, + SML_VOLT_SQUARED_HOURS = 48, + SML_AMPERE_SQUARED_HOURS = 49, + SML_KILOGRAM_PER_SECOND = 50, + SML_KELVIN = 52, + SML_VOLT_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 53, + SML_AMPERE_SQUARED_HOUR_METER_CONSTANT_OR_PULSE_VALUE = 54, + SML_METER_CONSTANT_OR_PULSE_VALUE = 55, + SML_PERCENTAGE = 56, + SML_AMPERE_HOUR = 57, + SML_ENERGY_PER_VOLUME = 60, + SML_CALORIFIC_VALUE = 61, + SML_MOLE_PERCENT = 62, + SML_MASS_DENSITY = 63, + SML_PASCAL_SECOND = 64, + SML_RESERVED = 253, + SML_OTHER_UNIT = 254, + SML_COUNT = 255 +} sml_units_t; + +sml_states_t smlState(unsigned char &byte); +bool smlOBISCheck(const unsigned char *obis); +void smlOBISManufacturer(unsigned char *str, int maxSize); +void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit); + +// Be aware that double on Arduino UNO is just 32 bit +void smlOBISWh(double &wh); +void smlOBISW(double &w); +void smlOBISVolt(double &v); +void smlOBISAmpere(double &a); + +#endif diff --git a/lib/SMLParser/smlCrcTable.h b/lib/SMLParser/smlCrcTable.h new file mode 100644 index 000000000..8f6c1c19d --- /dev/null +++ b/lib/SMLParser/smlCrcTable.h @@ -0,0 +1,42 @@ +#ifndef SML_CRC_TABLE_H +#define SML_CRC_TABLE_H + +#include + +#ifdef ARDUINO +#include +static const uint16_t smlCrcTable[256] PROGMEM = +#else +static const uint16_t smlCrcTable[256] = +#endif + {0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, + 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 0x1081, 0x0108, + 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 0x9CC9, 0x8D40, 0xBFDB, + 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 0x2102, 0x308B, 0x0210, 0x1399, + 0x6726, 0x76AF, 0x4434, 0x55BD, 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, + 0xFAE7, 0xC87C, 0xD9F5, 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, + 0x54B5, 0x453C, 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, + 0xC974, 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, + 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 0x5285, + 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 0xDECD, 0xCF44, + 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 0x6306, 0x728F, 0x4014, + 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, + 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, + 0x242A, 0x16B1, 0x0738, 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, + 0x9AF9, 0x8B70, 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, + 0xF0B7, 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, + 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 0x18C1, + 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 0xA50A, 0xB483, + 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 0x2942, 0x38CB, 0x0A50, + 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 0xB58B, 0xA402, 0x9699, 0x8710, + 0xF3AF, 0xE226, 0xD0BD, 0xC134, 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, + 0x6E6E, 0x5CF5, 0x4D7C, 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, + 0xA33A, 0xB2B3, 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, + 0x3EFB, 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, + 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 0xE70E, + 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 0x6B46, 0x7ACF, + 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 0xF78F, 0xE606, 0xD49D, + 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 0x7BC7, 0x6A4E, 0x58D5, 0x495C, + 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78}; + +#endif \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index c5cbca082..328ce6962 100644 --- a/platformio.ini +++ b/platformio.ini @@ -33,6 +33,7 @@ lib_deps = olikraus/U8g2 @ ^2.34.16 buelowp/sunset @ ^1.1.7 https://github.com/coryjfowler/MCP_CAN_lib + plerup/EspSoftwareSerial@^8.0.1 mobizt/FirebaseJson @ ^3.0.6 extra_scripts = diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index da2fc2d3b..ffe291011 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -10,11 +10,14 @@ #include "SDM.h" #include "MessageOutput.h" #include +#include PowerMeterClass PowerMeter; SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN); +SoftwareSerial inputSerial; + void PowerMeterClass::init() { using std::placeholders::_1; @@ -28,8 +31,12 @@ void PowerMeterClass::init() _lastPowerMeterUpdate = 0; CONFIG_T& config = Configuration.get(); + + if (!config.PowerMeter_Enabled) { + return; + } - if (config.PowerMeter_Enabled && config.PowerMeter_Source == 0) { + if (config.PowerMeter_Source == SOURCE_MQTT) { if (strlen(config.PowerMeter_MqttTopicPowerMeter1) > 0) { MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } @@ -43,16 +50,30 @@ void PowerMeterClass::init() } } - mqttInitDone = true; + if(config.PowerMeter_Source == SOURCE_SDM1PH || config.PowerMeter_Source == SOURCE_SDM3PH) { + sdm.begin(); + } + + if (config.PowerMeter_Source == SOURCE_HTTP) { + HttpPowerMeter.init(); + } - sdm.begin(); - HttpPowerMeter.init(); + if(config.PowerMeter_Source == SOURCE_SML) { + pinMode(SML_RX_PIN, INPUT); + inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95); + inputSerial.enableRx(true); + inputSerial.enableTx(false); + inputSerial.flush(); + } + + mqttInitDone = true; } void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { CONFIG_T& config = Configuration.get(); - if (config.PowerMeter_Enabled && config.PowerMeter_Source != SOURCE_MQTT) { + + if (!config.PowerMeter_Enabled || config.PowerMeter_Source != SOURCE_MQTT) { return; } @@ -96,8 +117,8 @@ void PowerMeterClass::mqtt() MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); - MqttSettings.publish(topic + "/import", String(_PowerMeterImport)); - MqttSettings.publish(topic + "/export", String(_PowerMeterExport)); + MqttSettings.publish(topic + "/import", String(_powerMeterImport)); + MqttSettings.publish(topic + "/export", String(_powerMeterExport)); } } @@ -105,6 +126,12 @@ void PowerMeterClass::loop() { CONFIG_T& config = Configuration.get(); + if (config.PowerMeter_Enabled && config.PowerMeter_Source == SOURCE_SML) { + if (!smlReadLoop()) { + return; + } + } + if (!config.PowerMeter_Enabled || (millis() - _lastPowerMeterCheck) < (config.PowerMeter_Interval * 1000)) { return; @@ -112,15 +139,15 @@ void PowerMeterClass::loop() uint8_t _address = config.PowerMeter_SdmAddress; - if (config.PowerMeter_Source== SOURCE_SDM1PH) { + if (config.PowerMeter_Source == SOURCE_SDM1PH) { _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); _powerMeter2Power = 0.0; _powerMeter3Power = 0.0; _powerMeter1Voltage = static_cast(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address)); _powerMeter2Voltage = 0.0; _powerMeter3Voltage = 0.0; - _PowerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); - _PowerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); + _powerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); + _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); _lastPowerMeterUpdate = millis(); } else if (config.PowerMeter_Source == SOURCE_SDM3PH) { @@ -130,8 +157,8 @@ void PowerMeterClass::loop() _powerMeter1Voltage = static_cast(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address)); _powerMeter2Voltage = static_cast(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address)); _powerMeter3Voltage = static_cast(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address)); - _PowerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); - _PowerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); + _powerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); + _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); _lastPowerMeterUpdate = millis(); } else if (config.PowerMeter_Source == SOURCE_HTTP) { @@ -149,3 +176,22 @@ void PowerMeterClass::loop() _lastPowerMeterCheck = millis(); } + +bool PowerMeterClass::smlReadLoop() +{ + while (inputSerial.available()) { + double readVal = 0; + unsigned char smlCurrentChar = inputSerial.read(); + sml_states_t smlCurrentState = smlState(smlCurrentChar); + if (smlCurrentState == SML_LISTEND) { + for(auto& handler: smlHandlerList) { + if (smlOBISCheck(handler.OBIS)) { + handler.Fn(readVal); + *handler.Arg = readVal; + } + } + } else if (smlCurrentState == SML_FINAL) + return true; + } + return false; +} diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index da25cf6b5..1fd5eb6d6 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -470,6 +470,7 @@ "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(S) + JSON", + "typeSML": "SML (OBIS 16.7.0)", "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 537db46a9..45498cef5 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -470,6 +470,7 @@ "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(s) + JSON", + "typeSML": "SML (OBIS 16.7.0)", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index c227afd85..fc45009d2 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -215,10 +215,11 @@ export default defineComponent({ dataLoading: true, powerMeterConfigList: {} as PowerMeterConfig, powerMeterSourceList: [ - { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, { key: 0, value: this.$t('powermeteradmin.typeMQTT') }, { key: 1, value: this.$t('powermeteradmin.typeSDM1ph') }, { key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, + { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, + { key: 4, value: this.$t('powermeteradmin.typeSML') }, ], alertMessage: "", alertType: "info",