diff --git a/README.md b/README.md index b5f5ea57..0617f3f1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Like to show your own build? Just send me a Pull Request. #### Power Limiter States -![PowerLimiterInverterStates](https://user-images.githubusercontent.com/59169507/224017473-9b3706f4-7c38-44f0-8890-7936fee67c7c.png) +![PowerLimiterInverterStates](https://github.com/helgeerbe/OpenDTU-OnBattery/blob/development/docs/PowerLimiterInverterStates.png) ### Web-Live-Interface: ![image](https://user-images.githubusercontent.com/59169507/187224107-4e0d0cab-2e1b-4e47-9410-a49f80aa6789.png) diff --git a/docs/PowerLimiterInverterStates.drawio b/docs/PowerLimiterInverterStates.drawio index d1cce508..43b5faf2 100644 --- a/docs/PowerLimiterInverterStates.drawio +++ b/docs/PowerLimiterInverterStates.drawio @@ -1,6 +1,6 @@ - + @@ -18,7 +18,7 @@ - + @@ -119,32 +119,19 @@ - - + + - - - - - - - - - - - + + - - - - - - - - - + + + + + @@ -157,43 +144,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + - + @@ -210,7 +172,7 @@ - + @@ -257,6 +219,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/PowerLimiterInverterStates.png b/docs/PowerLimiterInverterStates.png index feedcb95..47ab7e90 100644 Binary files a/docs/PowerLimiterInverterStates.png and b/docs/PowerLimiterInverterStates.png differ diff --git a/include/Configuration.h b/include/Configuration.h index 620c0d07..ca637c82 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -99,13 +99,20 @@ struct CONFIG_T { bool Mqtt_Hass_Expire; + bool PowerMeter_Enabled; + uint32_t PowerMeter_Interval; + uint32_t PowerMeter_Source; + char PowerMeter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerMeter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerMeter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; + uint32_t PowerMeter_SdmBaudrate; + uint32_t PowerMeter_SdmAddress; + + bool PowerLimiter_Enabled; bool PowerLimiter_SolarPassTroughEnabled; uint8_t PowerLimiter_BatteryDrainStategy; uint32_t PowerLimiter_Interval; - char PowerLimiter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; - char PowerLimiter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; - char PowerLimiter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; bool PowerLimiter_IsInverterBehindPowerMeter; uint8_t PowerLimiter_InverterId; uint8_t PowerLimiter_InverterChannelId; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index a2ad7ebf..ed1ff2ed 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -26,12 +26,10 @@ public: void loop(); plStates getPowerLimiterState(); int32_t getLastRequestedPowewrLimit(); - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); private: uint32_t _lastCommandSent; uint32_t _lastLoop; - uint32_t _lastPowerMeterUpdate; int32_t _lastRequestedPowerLimit; plStates _plState = STATE_DISCOVER; diff --git a/include/PowerMeter.h b/include/PowerMeter.h new file mode 100644 index 00000000..da677c0b --- /dev/null +++ b/include/PowerMeter.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include "SDM.h" + +#ifndef SDM_RX_PIN +#define SDM_RX_PIN 13 +#endif + +#ifndef SDM_TX_PIN +#define SDM_TX_PIN 32 +#endif + +class PowerMeterClass { +public: + void init(); + void mqtt(); + void loop(); + void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + float getPowerTotal(); + uint32_t getLastPowerMeterUpdate(); + +private: + uint32_t _interval; + uint32_t _lastPowerMeterUpdate; + + 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; + + bool mqttInitDone = false; +}; + +extern PowerMeterClass PowerMeter; diff --git a/include/WebApi.h b/include/WebApi.h index 9f223402..abdcc476 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -15,6 +15,7 @@ #include "WebApi_network.h" #include "WebApi_ntp.h" #include "WebApi_power.h" +#include "WebApi_powermeter.h" #include "WebApi_powerlimiter.h" #include "WebApi_prometheus.h" #include "WebApi_security.h" @@ -55,6 +56,7 @@ private: WebApiNetworkClass _webApiNetwork; WebApiNtpClass _webApiNtp; WebApiPowerClass _webApiPower; + WebApiPowerMeterClass _webApiPowerMeter; WebApiPowerLimiterClass _webApiPowerLimiter; WebApiPrometheusClass _webApiPrometheus; WebApiSecurityClass _webApiSecurity; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h new file mode 100644 index 00000000..2175029c --- /dev/null +++ b/include/WebApi_powermeter.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + + +class WebApiPowerMeterClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onStatus(AsyncWebServerRequest* request); + void onAdminGet(AsyncWebServerRequest* request); + void onAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index a365d50b..48b70b26 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -92,6 +92,13 @@ #define VEDIRECT_UPDATESONLY true #define VEDIRECT_POLL_INTERVAL 5 +#define POWERMETER_ENABLED false +#define POWERMETER_INTERVAL 10 +#define POWERMETER_SOURCE 2 +#define POWERMETER_SDMBAUDRATE 9600 +#define POWERMETER_SDMADDRESS 1 + + #define POWERLIMITER_ENABLED false #define POWERLIMITER_SOLAR_PASSTROUGH_ENABLED true #define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 24ea8833..343cdd0e 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -170,8 +170,8 @@ uint8_t DevInfoParser::getDevIdx() /* struct tm to seconds since Unix epoch */ time_t DevInfoParser::timegm(struct tm* t) { - register uint32_t year; - register time_t result; + uint32_t year; + time_t result; #define MONTHSPERYEAR 12 /* months per calendar year */ static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; diff --git a/lib/SdmEnergyMeter/SDM.cpp b/lib/SdmEnergyMeter/SDM.cpp new file mode 100644 index 00000000..48de0648 --- /dev/null +++ b/lib/SdmEnergyMeter/SDM.cpp @@ -0,0 +1,254 @@ +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. +* Reading via Hardware or Software Serial library & rs232<->rs485 converter +* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) +*/ +//------------------------------------------------------------------------------ +#include "SDM.h" +//------------------------------------------------------------------------------ +#if defined ( USE_HARDWARESERIAL ) +#if defined ( ESP8266 ) +SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, bool swapuart) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; + this->_swapuart = swapuart; +} +#elif defined ( ESP32 ) +SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; + this->_rx_pin = rx_pin; + this->_tx_pin = tx_pin; +} +#else +SDM::SDM(HardwareSerial& serial, long baud, int dere_pin, int config) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; +} +#endif +#else +#if defined ( ESP8266 ) || defined ( ESP32 ) +SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin, int config, int8_t rx_pin, int8_t tx_pin) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; + this->_config = config; + this->_rx_pin = rx_pin; + this->_tx_pin = tx_pin; +} +#else +SDM::SDM(SoftwareSerial& serial, long baud, int dere_pin) : sdmSer(serial) { + this->_baud = baud; + this->_dere_pin = dere_pin; +} +#endif +#endif + +SDM::~SDM() { +} + +void SDM::begin(void) { +#if defined ( USE_HARDWARESERIAL ) +#if defined ( ESP8266 ) + sdmSer.begin(_baud, (SerialConfig)_config); +#elif defined ( ESP32 ) + sdmSer.begin(_baud, _config, _rx_pin, _tx_pin); +#else + sdmSer.begin(_baud, _config); +#endif +#else +#if defined ( ESP8266 ) || defined ( ESP32 ) + sdmSer.begin(_baud, (SoftwareSerialConfig)_config, _rx_pin, _tx_pin); +#else + sdmSer.begin(_baud); +#endif +#endif + +#if defined ( USE_HARDWARESERIAL ) && defined ( ESP8266 ) + if (_swapuart) + sdmSer.swap(); +#endif + if (_dere_pin != NOT_A_PIN) { + pinMode(_dere_pin, OUTPUT); //set output pin mode for DE/RE pin when used (for control MAX485) + } + dereSet(LOW); //set init state to receive from SDM -> DE Disable, /RE Enable (for control MAX485) +} + +float SDM::readVal(uint16_t reg, uint8_t node) { + uint16_t temp; + unsigned long resptime; + uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0}; + float res = NAN; + uint16_t readErr = SDM_ERR_NO_ERROR; + + sdmarr[2] = highByte(reg); + sdmarr[3] = lowByte(reg); + + temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes + + sdmarr[6] = lowByte(temp); + sdmarr[7] = highByte(temp); + +#if !defined ( USE_HARDWARESERIAL ) + sdmSer.listen(); //enable softserial rx interrupt +#endif + + flush(); //read serial if any old data is available + + dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485) + + delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524 + + sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes + + sdmSer.flush(); //clear out tx buffer + + dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) + + resptime = millis(); + + while (sdmSer.available() < FRAMESIZE) { + if (millis() - resptime > msturnaround) { + readErr = SDM_ERR_TIMEOUT; //err debug (4) + break; + } + yield(); + } + + if (readErr == SDM_ERR_NO_ERROR) { //if no timeout... + + if (sdmSer.available() >= FRAMESIZE) { + + for(int n=0; n SDM_MAX_DELAY) + msturnaround = SDM_MAX_DELAY; + else + msturnaround = _msturnaround; +} + +void SDM::setMsTimeout(uint16_t _mstimeout) { + if (_mstimeout < SDM_MIN_DELAY) + mstimeout = SDM_MIN_DELAY; + else if (_mstimeout > SDM_MAX_DELAY) + mstimeout = SDM_MAX_DELAY; + else + mstimeout = _mstimeout; +} + +uint16_t SDM::getMsTurnaround() { + return (msturnaround); +} + +uint16_t SDM::getMsTimeout() { + return (mstimeout); +} + +uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) { + uint16_t _crc, _flag; + _crc = 0xFFFF; + for (uint8_t i = 0; i < len; i++) { + _crc ^= (uint16_t)array[i]; + for (uint8_t j = 8; j; j--) { + _flag = _crc & 0x0001; + _crc >>= 1; + if (_flag) + _crc ^= 0xA001; + } + } + return _crc; +} + +void SDM::flush(unsigned long _flushtime) { + unsigned long flushstart = millis(); + while (sdmSer.available() || (millis() - flushstart < _flushtime)) { + if (sdmSer.available()) //read serial if any old data is available + sdmSer.read(); + delay(1); + } +} + +void SDM::dereSet(bool _state) { + if (_dere_pin != NOT_A_PIN) + digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) +} diff --git a/lib/SdmEnergyMeter/SDM.h b/lib/SdmEnergyMeter/SDM.h new file mode 100644 index 00000000..7a24d124 --- /dev/null +++ b/lib/SdmEnergyMeter/SDM.h @@ -0,0 +1,299 @@ +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. +* Reading via Hardware or Software Serial library & rs232<->rs485 converter +* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) +*/ +//------------------------------------------------------------------------------ +#ifndef SDM_h +#define SDM_h +//------------------------------------------------------------------------------ +#include +#include +#if defined ( USE_HARDWARESERIAL ) + #include +#else + #include +#endif +//------------------------------------------------------------------------------ +//DEFAULT CONFIG (DO NOT CHANGE ANYTHING!!! for changes use SDM_Config_User.h): +//------------------------------------------------------------------------------ +#if !defined ( SDM_UART_BAUD ) + #define SDM_UART_BAUD 4800 // default baudrate +#endif + +#if !defined ( DERE_PIN ) + #define DERE_PIN NOT_A_PIN // default digital pin for control MAX485 DE/RE lines (connect DE & /RE together to this pin) +#endif + +#if defined ( USE_HARDWARESERIAL ) + + #if !defined ( SDM_UART_CONFIG ) + #define SDM_UART_CONFIG SERIAL_8N1 // default hardware uart config + #endif + + #if defined ( ESP8266 ) && !defined ( SWAPHWSERIAL ) + #define SWAPHWSERIAL 0 // (only esp8266) when hwserial used, then swap uart pins from 3/1 to 13/15 (default not swap) + #endif + + #if defined ( ESP32 ) + #if !defined ( SDM_RX_PIN ) + #define SDM_RX_PIN -1 // use default rx pin for selected port + #endif + #if !defined ( SDM_TX_PIN ) + #define SDM_TX_PIN -1 // use default tx pin for selected port + #endif + #endif + +#else + + #if defined ( ESP8266 ) || defined ( ESP32 ) + #if !defined ( SDM_UART_CONFIG ) + #define SDM_UART_CONFIG SWSERIAL_8N1 // default softwareware uart config for esp8266/esp32 + #endif + #endif + +// #if !defined ( SDM_RX_PIN ) || !defined ( SDM_TX_PIN ) +// #error "SDM_RX_PIN and SDM_TX_PIN must be defined in SDM_Config_User.h for Software Serial option)" +// #endif + + #if !defined ( SDM_RX_PIN ) + #define SDM_RX_PIN -1 + #endif + #if !defined ( SDM_TX_PIN ) + #define SDM_TX_PIN -1 + #endif + +#endif + +#if !defined ( WAITING_TURNAROUND_DELAY ) + #define WAITING_TURNAROUND_DELAY 200 // time in ms to wait for process current request +#endif + +#if !defined ( RESPONSE_TIMEOUT ) + #define RESPONSE_TIMEOUT 500 // time in ms to wait for return response from all devices before next request +#endif + +#if !defined ( SDM_MIN_DELAY ) + #define SDM_MIN_DELAY 20 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT +#endif + +#if !defined ( SDM_MAX_DELAY ) + #define SDM_MAX_DELAY 5000 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT +#endif + +//------------------------------------------------------------------------------ + +#define SDM_ERR_NO_ERROR 0 // no error +#define SDM_ERR_CRC_ERROR 1 // crc error +#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong +#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm +#define SDM_ERR_TIMEOUT 4 // timeout + +//------------------------------------------------------------------------------ + +#define FRAMESIZE 9 // size of out/in array +#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data + +#define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1) +#define SDM_B_02 0x04 // BYTE 2 -> function code (default value 0x04 read from 3X input registers) +#define SDM_B_05 0x00 // BYTE 5 +#define SDM_B_06 0x02 // BYTE 6 + // BYTES 3 & 4 (BELOW) + +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR SDM DEVICES | +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | SDM630 | SDM230 | SDM220 | SDM120CT| SDM120 | SDM72D | SDM72 V2| +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#define SDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_VOLTAGE 0x0002 // V | 1 | | | | | | 1 | +#define SDM_PHASE_3_VOLTAGE 0x0004 // V | 1 | | | | | | 1 | +#define SDM_PHASE_1_CURRENT 0x0006 // A | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_CURRENT 0x0008 // A | 1 | | | | | | 1 | +#define SDM_PHASE_3_CURRENT 0x000A // A | 1 | | | | | | 1 | +#define SDM_PHASE_1_POWER 0x000C // W | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_POWER 0x000E // W | 1 | | | | | | 1 | +#define SDM_PHASE_3_POWER 0x0010 // W | 1 | | | | | | 1 | +#define SDM_PHASE_1_APPARENT_POWER 0x0012 // VA | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_APPARENT_POWER 0x0014 // VA | 1 | | | | | | 1 | +#define SDM_PHASE_3_APPARENT_POWER 0x0016 // VA | 1 | | | | | | 1 | +#define SDM_PHASE_1_REACTIVE_POWER 0x0018 // VAr | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_REACTIVE_POWER 0x001A // VAr | 1 | | | | | | 1 | +#define SDM_PHASE_3_REACTIVE_POWER 0x001C // VAr | 1 | | | | | | 1 | +#define SDM_PHASE_1_POWER_FACTOR 0x001E // | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_PHASE_2_POWER_FACTOR 0x0020 // | 1 | | | | | | 1 | +#define SDM_PHASE_3_POWER_FACTOR 0x0022 // | 1 | | | | | | 1 | +#define SDM_PHASE_1_ANGLE 0x0024 // Degrees | 1 | 1 | 1 | 1 | | | | +#define SDM_PHASE_2_ANGLE 0x0026 // Degrees | 1 | | | | | | | +#define SDM_PHASE_3_ANGLE 0x0028 // Degrees | 1 | | | | | | | +#define SDM_AVERAGE_L_TO_N_VOLTS 0x002A // V | 1 | | | | | | 1 | +#define SDM_AVERAGE_LINE_CURRENT 0x002E // A | 1 | | | | | | 1 | +#define SDM_SUM_LINE_CURRENT 0x0030 // A | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_POWER 0x0034 // W | 1 | | | | | 1 | 1 | +#define SDM_TOTAL_SYSTEM_APPARENT_POWER 0x0038 // VA | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_REACTIVE_POWER 0x003C // VAr | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_POWER_FACTOR 0x003E // | 1 | | | | | | 1 | +#define SDM_TOTAL_SYSTEM_PHASE_ANGLE 0x0042 // Degrees | 1 | | | | | | | +#define SDM_FREQUENCY 0x0046 // Hz | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_IMPORT_ACTIVE_ENERGY 0x0048 // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_EXPORT_ACTIVE_ENERGY 0x004A // kWh/MWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_IMPORT_REACTIVE_ENERGY 0x004C // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | | +#define SDM_EXPORT_REACTIVE_ENERGY 0x004E // kVArh/MVArh | 1 | 1 | 1 | 1 | 1 | | | +#define SDM_VAH_SINCE_LAST_RESET 0x0050 // kVAh/MVAh | 1 | | | | | | | +#define SDM_AH_SINCE_LAST_RESET 0x0052 // Ah/kAh | 1 | | | | | | | +#define SDM_TOTAL_SYSTEM_POWER_DEMAND 0x0054 // W | 1 | 1 | | | | | | +#define SDM_MAXIMUM_TOTAL_SYSTEM_POWER_DEMAND 0x0056 // W | 1 | 1 | | | | | | +#define SDM_CURRENT_SYSTEM_POSITIVE_POWER_DEMAND 0x0058 // W | | 1 | | | | | | +#define SDM_MAXIMUM_SYSTEM_POSITIVE_POWER_DEMAND 0x005A // W | | 1 | | | | | | +#define SDM_CURRENT_SYSTEM_REVERSE_POWER_DEMAND 0x005C // W | | 1 | | | | | | +#define SDM_MAXIMUM_SYSTEM_REVERSE_POWER_DEMAND 0x005E // W | | 1 | | | | | | +#define SDM_TOTAL_SYSTEM_VA_DEMAND 0x0064 // VA | 1 | | | | | | | +#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | | +#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | | +#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | | +#define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 | +#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 | +#define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 | +#define SDM_AVERAGE_LINE_TO_LINE_VOLTS 0x00CE // V | 1 | | | | | | 1 | +#define SDM_NEUTRAL_CURRENT 0x00E0 // A | 1 | | | | | | 1 | +#define SDM_PHASE_1_LN_VOLTS_THD 0x00EA // % | 1 | | | | | | | +#define SDM_PHASE_2_LN_VOLTS_THD 0x00EC // % | 1 | | | | | | | +#define SDM_PHASE_3_LN_VOLTS_THD 0x00EE // % | 1 | | | | | | | +#define SDM_PHASE_1_CURRENT_THD 0x00F0 // % | 1 | | | | | | | +#define SDM_PHASE_2_CURRENT_THD 0x00F2 // % | 1 | | | | | | | +#define SDM_PHASE_3_CURRENT_THD 0x00F4 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_TO_NEUTRAL_VOLTS_THD 0x00F8 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_CURRENT_THD 0x00FA // % | 1 | | | | | | | +#define SDM_TOTAL_SYSTEM_POWER_FACTOR_INV 0x00FE // | 1 | | | | | | | +#define SDM_PHASE_1_CURRENT_DEMAND 0x0102 // A | 1 | 1 | | | | | | +#define SDM_PHASE_2_CURRENT_DEMAND 0x0104 // A | 1 | | | | | | | +#define SDM_PHASE_3_CURRENT_DEMAND 0x0106 // A | 1 | | | | | | | +#define SDM_MAXIMUM_PHASE_1_CURRENT_DEMAND 0x0108 // A | 1 | 1 | | | | | | +#define SDM_MAXIMUM_PHASE_2_CURRENT_DEMAND 0x010A // A | 1 | | | | | | | +#define SDM_MAXIMUM_PHASE_3_CURRENT_DEMAND 0x010C // A | 1 | | | | | | | +#define SDM_LINE_1_TO_LINE_2_VOLTS_THD 0x014E // % | 1 | | | | | | | +#define SDM_LINE_2_TO_LINE_3_VOLTS_THD 0x0150 // % | 1 | | | | | | | +#define SDM_LINE_3_TO_LINE_1_VOLTS_THD 0x0152 // % | 1 | | | | | | | +#define SDM_AVERAGE_LINE_TO_LINE_VOLTS_THD 0x0154 // % | 1 | | | | | | | +#define SDM_TOTAL_ACTIVE_ENERGY 0x0156 // kWh | 1 | 1 | 1 | 1 | 1 | 1 | 1 | +#define SDM_TOTAL_REACTIVE_ENERGY 0x0158 // kVArh | 1 | 1 | 1 | 1 | 1 | | 1 | +#define SDM_L1_IMPORT_ACTIVE_ENERGY 0x015A // kWh | 1 | | | | | | | +#define SDM_L2_IMPORT_ACTIVE_ENERGY 0x015C // kWh | 1 | | | | | | | +#define SDM_L3_IMPORT_ACTIVE_ENERGY 0x015E // kWh | 1 | | | | | | | +#define SDM_L1_EXPORT_ACTIVE_ENERGY 0x0160 // kWh | 1 | | | | | | | +#define SDM_L2_EXPORT_ACTIVE_ENERGY 0x0162 // kWh | 1 | | | | | | | +#define SDM_L3_EXPORT_ACTIVE_ENERGY 0x0164 // kWh | 1 | | | | | | | +#define SDM_L1_TOTAL_ACTIVE_ENERGY 0x0166 // kWh | 1 | | | | | | | +#define SDM_L2_TOTAL_ACTIVE_ENERGY 0x0168 // kWh | 1 | | | | | | | +#define SDM_L3_TOTAL_ACTIVE_ENERGY 0x016a // kWh | 1 | | | | | | | +#define SDM_L1_IMPORT_REACTIVE_ENERGY 0x016C // kVArh | 1 | | | | | | | +#define SDM_L2_IMPORT_REACTIVE_ENERGY 0x016E // kVArh | 1 | | | | | | | +#define SDM_L3_IMPORT_REACTIVE_ENERGY 0x0170 // kVArh | 1 | | | | | | | +#define SDM_L1_EXPORT_REACTIVE_ENERGY 0x0172 // kVArh | 1 | | | | | | | +#define SDM_L2_EXPORT_REACTIVE_ENERGY 0x0174 // kVArh | 1 | | | | | | | +#define SDM_L3_EXPORT_REACTIVE_ENERGY 0x0176 // kVArh | 1 | | | | | | | +#define SDM_L1_TOTAL_REACTIVE_ENERGY 0x0178 // kVArh | 1 | | | | | | | +#define SDM_L2_TOTAL_REACTIVE_ENERGY 0x017A // kVArh | 1 | | | | | | | +#define SDM_L3_TOTAL_REACTIVE_ENERGY 0x017C // kVArh | 1 | | | | | | | +#define SDM_CURRENT_RESETTABLE_TOTAL_ACTIVE_ENERGY 0x0180 // kWh | | 1 | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | | +#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 | +#define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 | +#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 | +#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 | +//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DDM DEVICE | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DDM18SD | +//--------------------------------------------------------------------------------------------------------- +#define DDM_PHASE_1_VOLTAGE 0x0000 // V | 1 | +#define DDM_PHASE_1_CURRENT 0x0008 // A | 1 | +#define DDM_PHASE_1_POWER 0x0012 // W | 1 | +#define DDM_PHASE_1_REACTIVE_POWER 0x001A // VAr | 1 | +#define DDM_PHASE_1_POWER_FACTOR 0x002A // | 1 | +#define DDM_FREQUENCY 0x0036 // Hz | 1 | +#define DDM_IMPORT_ACTIVE_ENERGY 0x0100 // kWh | 1 | +#define DDM_IMPORT_REACTIVE_ENERGY 0x0400 // kVArh | 1 | +//--------------------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DEVNAME DEVICE | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME | +//--------------------------------------------------------------------------------------------------------- +//#define DEVNAME_VOLTAGE 0x0000 // V | 1 | +//#define DEVNAME_CURRENT 0x0002 // A | 1 | +//#define DEVNAME_POWER 0x0004 // W | 1 | +//--------------------------------------------------------------------------------------------------------- + +//----------------------------------------------------------------------------------------------------------------------------------------------------------- + +class SDM { + public: +#if defined ( USE_HARDWARESERIAL ) // hardware serial + #if defined ( ESP8266 ) // on esp8266 + SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, bool swapuart = SWAPHWSERIAL); + #elif defined ( ESP32 ) // on esp32 + SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN); + #else // on avr + SDM(HardwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG); + #endif +#else // software serial + #if defined ( ESP8266 ) || defined ( ESP32 ) // on esp8266/esp32 + SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN, int config = SDM_UART_CONFIG, int8_t rx_pin = SDM_RX_PIN, int8_t tx_pin = SDM_TX_PIN); + #else // on avr + SDM(SoftwareSerial& serial, long baud = SDM_UART_BAUD, int dere_pin = DERE_PIN); + #endif +#endif + virtual ~SDM(); + + void begin(void); + float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node + uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase) + uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase) + uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false) + void clearErrCode(); // clear last errorcode + void clearErrCount(); // clear total errors count + void clearSuccCount(); // clear total success count + void setMsTurnaround(uint16_t _msturnaround = WAITING_TURNAROUND_DELAY); // set new value for WAITING_TURNAROUND_DELAY (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY + void setMsTimeout(uint16_t _mstimeout = RESPONSE_TIMEOUT); // set new value for RESPONSE_TIMEOUT (ms), min=SDM_MIN_DELAY, max=SDM_MAX_DELAY + uint16_t getMsTurnaround(); // get current value of WAITING_TURNAROUND_DELAY (ms) + uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms) + + private: +#if defined ( USE_HARDWARESERIAL ) + HardwareSerial& sdmSer; +#else + SoftwareSerial& sdmSer; +#endif + +#if defined ( USE_HARDWARESERIAL ) + int _config = SDM_UART_CONFIG; + #if defined ( ESP8266 ) + bool _swapuart = SWAPHWSERIAL; + #elif defined ( ESP32 ) + int8_t _rx_pin = -1; + int8_t _tx_pin = -1; + #endif +#else + #if defined ( ESP8266 ) || defined ( ESP32 ) + int _config = SDM_UART_CONFIG; + #endif + int8_t _rx_pin = -1; + int8_t _tx_pin = -1; +#endif + long _baud = SDM_UART_BAUD; + int _dere_pin = DERE_PIN; + uint16_t readingerrcode = SDM_ERR_NO_ERROR; // 4 = timeout; 3 = not enough bytes; 2 = number of bytes OK but bytes b0,b1 or b2 wrong, 1 = crc error + uint16_t msturnaround = WAITING_TURNAROUND_DELAY; + uint16_t mstimeout = RESPONSE_TIMEOUT; + uint32_t readingerrcount = 0; // total errors counter + uint32_t readingsuccesscount = 0; // total success counter + uint16_t calculateCRC(uint8_t *array, uint8_t len); + void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms + void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM +}; +#endif // SDM_h diff --git a/lib/SdmEnergyMeter/SDM_Config_User.h b/lib/SdmEnergyMeter/SDM_Config_User.h new file mode 100644 index 00000000..01ff257f --- /dev/null +++ b/lib/SdmEnergyMeter/SDM_Config_User.h @@ -0,0 +1,93 @@ +/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. +* Reading via Hardware or Software Serial library & rs232<->rs485 converter +* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) +*/ + +/* +* USER CONFIG: +*/ + +//------------------------------------------------------------------------------ + +/* +* define or undefine USE_HARDWARESERIAL (uncomment only one or none) +*/ +//#undef USE_HARDWARESERIAL +#define USE_HARDWARESERIAL + +//------------------------------------------------------------------------------ + +/* +* define user baudrate +*/ +#define SDM_UART_BAUD 9600 + +//------------------------------------------------------------------------------ + +/* +* define user SDM_RX_PIN and SDM_TX_PIN for esp/avr Software Serial option +* or ESP32 with Hardware Serial if default core pins are not suitable +*/ +#if defined ( USE_HARDWARESERIAL ) + #if defined ( ESP32 ) + #define SDM_RX_PIN 13 + #define SDM_TX_PIN 32 + #endif +#else + #if defined ( ESP8266 ) || defined ( ESP32 ) + #define SDM_RX_PIN 13 + #define SDM_TX_PIN 15 + #else + #define SDM_RX_PIN 10 + #define SDM_TX_PIN 11 + #endif +#endif + +//------------------------------------------------------------------------------ + +/* +* define user DERE_PIN for control MAX485 DE/RE lines (connect DE & /RE together to this pin) +*/ +//#define DERE_PIN NOT_A_PIN + +//------------------------------------------------------------------------------ + +#if defined ( USE_HARDWARESERIAL ) + + /* + * define user SDM_UART_CONFIG for hardware serial + */ + //#define SDM_UART_CONFIG SERIAL_8N1 + + //---------------------------------------------------------------------------- + + /* + * define user SWAPHWSERIAL, if true(1) then swap uart pins from 3/1 to 13/15 (only ESP8266) + */ + //#define SWAPHWSERIAL 0 + +#else + + /* + * define user SDM_UART_CONFIG for software serial + */ + //#define SDM_UART_CONFIG SWSERIAL_8N1 + +#endif + +//------------------------------------------------------------------------------ + +/* +* define user WAITING_TURNAROUND_DELAY time in ms to wait for process current request +*/ +//#define WAITING_TURNAROUND_DELAY 200 + +//------------------------------------------------------------------------------ + +/* +* define user RESPONSE_TIMEOUT time in ms to wait for return response from all devices before next request +*/ +//#define RESPONSE_TIMEOUT 500 + +//------------------------------------------------------------------------------ diff --git a/platformio.ini b/platformio.ini index 97272cda..9b387d3a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -20,13 +20,17 @@ platform = espressif32@>=6.0.1 build_flags = -DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz -Wall -Wextra -Werror + -std=c++17 + -std=gnu++17 +build_unflags = + -std=gnu++11 lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer bblanchon/ArduinoJson @ ^6.21.0 https://github.com/bertmelis/espMqttClient.git#v1.4.1 nrf24/RF24 @ ^1.4.5 - olikraus/U8g2 @ ^2.34.13 + olikraus/U8g2 @ ^2.34.16 buelowp/sunset @ ^1.1.7 extra_scripts = diff --git a/platformio_override.ini b/platformio_override.ini index 48155240..aab12e37 100644 --- a/platformio_override.ini +++ b/platformio_override.ini @@ -18,7 +18,6 @@ ;upload_port = COM4 - ; you can define your personal board and/or settings here ; non functional example: diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d815745f..168beba1 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -115,14 +115,21 @@ bool ConfigurationClass::write() vedirect["updates_only"] = config.Vedirect_UpdatesOnly; vedirect["poll_interval"] = config.Vedirect_PollInterval; + JsonObject powermeter = doc.createNestedObject("powermeter"); + powermeter["enabled"] = config.PowerMeter_Enabled; + powermeter["interval"] = config.PowerMeter_Interval; + powermeter["source"] = config.PowerMeter_Source; + powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter_MqttTopicPowerMeter1; + powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter_MqttTopicPowerMeter2; + powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3; + powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate; + powermeter["sdmaddress"] = config.PowerMeter_SdmAddress; + JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); powerlimiter["enabled"] = config.PowerLimiter_Enabled; powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; powerlimiter["interval"] = config.PowerLimiter_Interval; - powerlimiter["mqtt_topic_powermeter_1"] = config.PowerLimiter_MqttTopicPowerMeter1; - powerlimiter["mqtt_topic_powermeter_2"] = config.PowerLimiter_MqttTopicPowerMeter2; - powerlimiter["mqtt_topic_powermeter_3"] = config.PowerLimiter_MqttTopicPowerMeter3; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter_IsInverterBehindPowerMeter; powerlimiter["inverter_id"] = config.PowerLimiter_InverterId; powerlimiter["inverter_channel_id"] = config.PowerLimiter_InverterChannelId; @@ -281,14 +288,22 @@ bool ConfigurationClass::read() config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL; + JsonObject powermeter = doc["powermeter"]; + config.PowerMeter_Enabled = powermeter["enabled"] | POWERMETER_ENABLED; + config.PowerMeter_Interval = powermeter["interval"] | POWERMETER_INTERVAL; + config.PowerMeter_Source = powermeter["source"] | POWERMETER_SOURCE; + strlcpy(config.PowerMeter_MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter1)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter2)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; + config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; + + JsonObject powerlimiter = doc["powerlimiter"]; config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; - strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, powerlimiter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter1)); - strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, powerlimiter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter2)); - strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, powerlimiter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter3)); config.PowerLimiter_IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; config.PowerLimiter_InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; config.PowerLimiter_InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 67196fec..daba917b 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -126,9 +126,12 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv return; } - MqttSettings.publish(topic, String( + String value = String( inv->Statistics()->getChannelFieldValue(type, channel, fieldId), - static_cast(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId)))); + static_cast(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId))); + value.trim(); + + MqttSettings.publish(topic, value); } String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 56f0711b..b9b5b171 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -4,6 +4,7 @@ */ #include "Battery.h" +#include "PowerMeter.h" #include "PowerLimiter.h" #include "Configuration.h" #include "MqttSettings.h" @@ -16,60 +17,17 @@ PowerLimiterClass PowerLimiter; void PowerLimiterClass::init() { - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - - CONFIG_T& config = Configuration.get(); - - // Zero export power limiter - if (strlen(config.PowerLimiter_MqttTopicPowerMeter1) != 0) { - MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter1, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - } - - if (strlen(config.PowerLimiter_MqttTopicPowerMeter2) != 0) { - MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter2, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - } - - if (strlen(config.PowerLimiter_MqttTopicPowerMeter3) != 0) { - MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - } - - _lastCommandSent = 0; + _lastCommandSent = 0; _lastLoop = 0; - _lastPowerMeterUpdate = 0; _lastRequestedPowerLimit = 0; } -void PowerLimiterClass::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 (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter1) == 0) { - _powerMeter1Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); - } - - if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter2) == 0) { - _powerMeter2Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); - } - - if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter3) == 0) { - _powerMeter3Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); - } - - _lastPowerMeterUpdate = millis(); -} - void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); if (!config.PowerLimiter_Enabled - || !MqttSettings.getConnected() + || !config.PowerMeter_Enabled || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { @@ -93,7 +51,7 @@ void PowerLimiterClass::loop() return; } - if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { + if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); } @@ -209,7 +167,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve { CONFIG_T& config = Configuration.get(); - int32_t newPowerLimit = round(_powerMeter1Power + _powerMeter2Power + _powerMeter3Power); + int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); int32_t victronChargePower = this->getDirectSolarPower(); @@ -218,7 +176,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); - if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { + if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { if (config.PowerLimiter_IsInverterBehindPowerMeter) { // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp new file mode 100644 index 00000000..19eac501 --- /dev/null +++ b/src/PowerMeter.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "PowerMeter.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include "SDM.h" +#include "MessageOutput.h" +#include + +PowerMeterClass PowerMeter; + +SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN); + +void PowerMeterClass::init() +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + _lastPowerMeterUpdate = 0; + + CONFIG_T& config = Configuration.get(); + + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter2, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter3, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + + mqttInitDone = true; + + sdm.begin(); +} + +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 == 0){ + + if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter1) == 0) { + _powerMeter1Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); + } + + if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter2) == 0) { + _powerMeter2Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); + } + + if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter3) == 0) { + _powerMeter3Power = std::stof(std::string(reinterpret_cast(payload), (unsigned int)len)); + } + + MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal()); + } + + _lastPowerMeterUpdate = millis(); +} + +float PowerMeterClass::getPowerTotal(){ + return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; +} + +uint32_t PowerMeterClass::getLastPowerMeterUpdate(){ + return _lastPowerMeterUpdate; +} + +void PowerMeterClass::mqtt(){ + if (!MqttSettings.getConnected()){ + return; + }else{ + String topic = "powermeter"; + MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); + MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); + MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); + MqttSettings.publish(topic + "/powertotal", String(getPowerTotal())); + 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)); + } +} + +void PowerMeterClass::loop() +{ + CONFIG_T& config = Configuration.get(); + + if(config.PowerMeter_Enabled && millis() - _lastPowerMeterUpdate >= (config.PowerMeter_Interval * 1000)){ + uint8_t _address = config.PowerMeter_SdmAddress; + if(config.PowerMeter_Source == 1){ + _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)); + } + if(config.PowerMeter_Source == 2){ + _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); + _powerMeter2Power = static_cast(sdm.readVal(SDM_PHASE_2_POWER, _address)); + _powerMeter3Power = static_cast(sdm.readVal(SDM_PHASE_3_POWER, _address)); + _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)); + } + + MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal()); + + mqtt(); + + _lastPowerMeterUpdate = millis(); + } +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index dad4db24..2a258a1e 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -31,6 +31,7 @@ void WebApiClass::init() _webApiNetwork.init(&_server); _webApiNtp.init(&_server); _webApiPower.init(&_server); + _webApiPowerMeter.init(&_server); _webApiPowerLimiter.init(&_server); _webApiPrometheus.init(&_server); _webApiSecurity.init(&_server); @@ -60,6 +61,7 @@ void WebApiClass::loop() _webApiNetwork.loop(); _webApiNtp.loop(); _webApiPower.loop(); + _webApiPowerMeter.loop(); _webApiPowerLimiter.loop(); _webApiSecurity.loop(); _webApiSysstatus.loop(); diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index de0fe908..a556f004 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -10,6 +10,8 @@ #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" +#include "PowerLimiter.h" +#include "PowerMeter.h" #include void WebApiMqttClass::init(AsyncWebServer* server) @@ -318,6 +320,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) MqttSettings.performReconnect(); MqttHandleHass.forceUpdate(); MqttHandleVedirectHass.forceUpdate(); + PowerMeter.init(); + PowerLimiter.init(); } String WebApiMqttClass::getRootCaCertInfo(const char* cert) diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 08f08355..c1015240 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -8,7 +8,9 @@ #include "AsyncJson.h" #include "Configuration.h" #include "MqttHandleHass.h" +#include "MqttHandleVedirectHass.h" #include "MqttSettings.h" +#include "PowerMeter.h" #include "PowerLimiter.h" #include "WebApi.h" #include "helper.h" @@ -38,9 +40,6 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root[F("enabled")] = config.PowerLimiter_Enabled; root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; - root[F("mqtt_topic_powermeter_1")] = config.PowerLimiter_MqttTopicPowerMeter1; - root[F("mqtt_topic_powermeter_2")] = config.PowerLimiter_MqttTopicPowerMeter2; - root[F("mqtt_topic_powermeter_3")] = config.PowerLimiter_MqttTopicPowerMeter3; root[F("is_inverter_behind_powermeter")] = config.PowerLimiter_IsInverterBehindPowerMeter; root[F("inverter_id")] = config.PowerLimiter_InverterId; root[F("inverter_channel_id")] = config.PowerLimiter_InverterChannelId; @@ -122,9 +121,6 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter_Enabled = root[F("enabled")].as(); config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as(); config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); - strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter1)); - strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter2)); - strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter3)); config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as(); config.PowerLimiter_InverterId = root[F("inverter_id")].as(); config.PowerLimiter_InverterChannelId = root[F("inverter_channel_id")].as(); @@ -146,7 +142,4 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - - MqttSettings.performReconnect(); // TODO(helge) is this really needed - PowerLimiter.init(); } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp new file mode 100644 index 00000000..9d8bd1fa --- /dev/null +++ b/src/WebApi_powermeter.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_powermeter.h" +#include "VeDirectFrameHandler.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "MqttHandleVedirectHass.h" +#include "MqttHandleHass.h" +#include "MqttSettings.h" +#include "PowerLimiter.h" +#include "PowerMeter.h" +#include "WebApi.h" +#include "helper.h" + +void WebApiPowerMeterClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1)); + _server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1)); + _server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1)); +} + +void WebApiPowerMeterClass::loop() +{ +} + +void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + root[F("enabled")] = config.PowerMeter_Enabled; + root[F("source")] = config.PowerMeter_Source; + root[F("interval")] = config.PowerMeter_Interval; + root[F("mqtt_topic_powermeter_1")] = config.PowerMeter_MqttTopicPowerMeter1; + root[F("mqtt_topic_powermeter_2")] = config.PowerMeter_MqttTopicPowerMeter2; + root[F("mqtt_topic_powermeter_3")] = config.PowerMeter_MqttTopicPowerMeter3; + root[F("sdmbaudrate")] = config.PowerMeter_SdmBaudrate; + root[F("sdmaddress")] = config.PowerMeter_SdmAddress; + + response->setLength(); + request->send(response); +} + +void WebApiPowerMeterClass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + this->onStatus(request); +} + +void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("enabled") && root.containsKey("source"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + config.PowerMeter_Enabled = root[F("enabled")].as(); + config.PowerMeter_Source = root[F("source")].as(); + config.PowerMeter_Interval = root[F("interval")].as(); + strlcpy(config.PowerMeter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter1)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter2)); + strlcpy(config.PowerMeter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + config.PowerMeter_SdmBaudrate = root[F("sdmbaudrate")].as(); + config.PowerMeter_SdmAddress = root[F("sdmaddress")].as(); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); + + yield(); + delay(1000); + yield(); + ESP.restart(); +} diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 4ade13f7..281f2446 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -61,14 +61,16 @@ void WebApiWsLiveClass::loop() if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { try { - DynamicJsonDocument root(40960); - JsonVariant var = root; - generateJsonResponse(var); - String buffer; - if (buffer) { + // free JsonDocument as soon as possible + { + DynamicJsonDocument root(40960); + JsonVariant var = root; + generateJsonResponse(var); serializeJson(root, buffer); + } + if (buffer) { if (Configuration.get().Security_AllowReadonly) { _ws.setAuthentication("", ""); } else { diff --git a/src/main.cpp b/src/main.cpp index b96a6aa9..1c5ee54a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include "SunPosition.h" #include "Utils.h" #include "WebApi.h" +#include "PowerMeter.h" #include "PowerLimiter.h" #include "defaults.h" #include @@ -146,6 +147,8 @@ void setup() } else { MessageOutput.println(F("Invalid pin config")); } + // Power meter + PowerMeter.init(); // Dynamic power limiter PowerLimiter.init(); @@ -165,6 +168,8 @@ void loop() { NetworkSettings.loop(); yield(); + PowerMeter.loop(); + yield(); PowerLimiter.loop(); yield(); InverterSettings.loop(); diff --git a/webapp/package.json b/webapp/package.json index 27bad5ac..c05a2052 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -21,7 +21,7 @@ "vue-router": "^4.1.6" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^0.9.2", + "@intlify/unplugin-vue-i18n": "^0.9.3", "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.6", "@types/node": "^18.15.3", @@ -34,8 +34,8 @@ "npm-run-all": "^4.1.5", "sass": "^1.59.3", "terser": "^5.16.6", - "typescript": "^4.9.5", - "vite": "^4.2.0", + "typescript": "^5.0.2", + "vite": "^4.2.1", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.1.0", "vue-tsc": "^1.2.0" diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 033e213b..587af22d 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -51,6 +51,9 @@
  • {{ $t('menu.VedirectSettings') }}
  • +
  • + {{ $t('menu.PowerMeterSettings') }} +
  • Dynamic Power Limiter
  • diff --git a/webapp/src/components/PinInfo.vue b/webapp/src/components/PinInfo.vue index 0770e3d5..e26bc6a4 100644 --- a/webapp/src/components/PinInfo.vue +++ b/webapp/src/components/PinInfo.vue @@ -11,8 +11,8 @@ -