diff --git a/include/Configuration.h b/include/Configuration.h index 620c0d074..ca637c82d 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 a2ad7ebfd..e34a5e04e 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -26,7 +26,6 @@ class PowerLimiterClass { 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; diff --git a/include/PowerMeter.h b/include/PowerMeter.h new file mode 100644 index 000000000..c0ae46407 --- /dev/null +++ b/include/PowerMeter.h @@ -0,0 +1,48 @@ +// 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(); + +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; + char PowerMeter_MqttTopicPowerMeter1old[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerMeter_MqttTopicPowerMeter2old[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerMeter_MqttTopicPowerMeter3old[MQTT_MAX_TOPIC_STRLEN + 1]; + +}; + +extern PowerMeterClass PowerMeter; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h new file mode 100644 index 000000000..2175029cb --- /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 a365d50b5..48b70b262 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/SdmEnergyMeter/SDM.cpp b/lib/SdmEnergyMeter/SDM.cpp new file mode 100644 index 000000000..48de0648d --- /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 000000000..7a24d1245 --- /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 000000000..01ff257ff --- /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/src/Configuration.cpp b/src/Configuration.cpp index d815745f7..0cc210dd4 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; + config.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; + config.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/PowerLimiter.cpp b/src/PowerLimiter.cpp index 56f0711bd..aacce8974 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,18 @@ 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)) { @@ -209,7 +168,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(); diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp new file mode 100644 index 000000000..c23d4e298 --- /dev/null +++ b/src/PowerMeter.cpp @@ -0,0 +1,142 @@ +// 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 + +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(); +//if(!mqttInitDone){ + if (strlen(config.PowerMeter_MqttTopicPowerMeter1) != 0 && !strcmp(PowerMeter_MqttTopicPowerMeter1old, config.PowerMeter_MqttTopicPowerMeter1)) { + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + strlcpy(PowerMeter_MqttTopicPowerMeter1old, config.PowerMeter_MqttTopicPowerMeter1, sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + } + + if (strlen(config.PowerMeter_MqttTopicPowerMeter2) != 0 && !strcmp(PowerMeter_MqttTopicPowerMeter2old, config.PowerMeter_MqttTopicPowerMeter2)) { + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter2, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + strlcpy(PowerMeter_MqttTopicPowerMeter2old, config.PowerMeter_MqttTopicPowerMeter2, sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + } + + if (strlen(config.PowerMeter_MqttTopicPowerMeter3) != 0 && !strcmp(PowerMeter_MqttTopicPowerMeter3old, config.PowerMeter_MqttTopicPowerMeter3)) { + MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter3, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + strlcpy(PowerMeter_MqttTopicPowerMeter3old, config.PowerMeter_MqttTopicPowerMeter3, sizeof(config.PowerMeter_MqttTopicPowerMeter3)); + } + mqttInitDone = true; +//} + sdm.begin(); + /*if(config.PowerMeter_Source != 0){ + if (strlen(config.PowerMeter_MqttTopicPowerMeter1) != 0) { + MqttSettings.unsubscribe(config.PowerMeter_MqttTopicPowerMeter1); + } + + if (strlen(config.PowerMeter_MqttTopicPowerMeter2) != 0) { + MqttSettings.unsubscribe(config.PowerMeter_MqttTopicPowerMeter2); + } + + if (strlen(config.PowerMeter_MqttTopicPowerMeter3) != 0) { + MqttSettings.unsubscribe(config.PowerMeter_MqttTopicPowerMeter3); + } + Hoymiles.getMessageOutput()->printf("PowerMeterClass: MQTT unsubscribed\n"); + }*/ + +} + +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){ + Hoymiles.getMessageOutput()->printf("PowerMeterClass: Received MQTT message on topic: %s\n", topic); + + 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)); + } + + Hoymiles.getMessageOutput()->printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal()); + } + + _lastPowerMeterUpdate = millis(); +} + +float PowerMeterClass::getPowerTotal(){ + return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; +} + +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)); + } + + Hoymiles.getMessageOutput()->printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal()); + + mqtt(); + + _lastPowerMeterUpdate = millis(); + + } +} diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 08f08355d..9e518cdd2 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -38,9 +38,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 +119,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(); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp new file mode 100644 index 000000000..d7bb776c1 --- /dev/null +++ b/src/WebApi_powermeter.cpp @@ -0,0 +1,123 @@ +// 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 "MqttHandleHass.h" +#include "MqttSettings.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); + + PowerMeter.init(); + MqttSettings.performReconnect(); + MqttHandleHass.forceUpdate(); + +} diff --git a/src/main.cpp b/src/main.cpp index b96a6aa97..1c5ee54a4 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/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 033e213b8..587af22df 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/locales/de.json b/webapp/src/locales/de.json index 82f7437ff..d0cee76d2 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -452,6 +452,24 @@ "UpdatesOnly": "Nur Änderungen senden:", "Save": "@:dtuadmin.Save" }, + "powermeteradmin":{ + "PowerMeterSettings": "Stromzähler Einstellungen", + "PowerMeterConfiguration": "Stromzähler Konfiguration", + "PowerMeterEnable": "Aktiviere Stromzähler", + "PowerMeterParameter": "Power Meter Parameter", + "PowerMeterSource": "Stromzählertyp", + "MQTT": "MQTT Konfiguration", + "typeMQTT": "MQTT", + "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", + "typeSDM3ph": "SDM 3 phase (SDM72/630)", + "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", + "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", + "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", + "SDM": "SDM-Stromzähler Konfiguration", + "sdmbaudrate": "Baudrate", + "sdmaddress": "Modbus Adresse", + "Save": "@:dtuadmin.Save" + }, "powerlimiteradmin": { "PowerLimiterSettings": "Power Limiter Einstellungen", "PowerLimiterConfiguration": "Power Limiter Konfiguration", @@ -473,9 +491,6 @@ "LowerPowerLimit": "Unteres Leistungslimit", "UpperPowerLimit": "Oberes Leistungslimit", "PowerMeters": "Leistungsmesser - MQTT", - "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", - "MqttTopicPowerMeter2": "MQTT topic - Power meter #2 (Optional)", - "MqttTopicPowerMeter3": "MQTT topic - Power meter #3 (Optional)", "BatterySocStartThreshold": "Akku SOC - Start", "BatterySocStopThreshold": "Akku SOC - Stop", "VoltageStartThreshold": "DC Spannung - Start", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 670213dd3..239a6f128 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -10,6 +10,7 @@ "DTUSettings": "DTU Settings", "DeviceManager": "Device-Manager", "VedirectSettings": "Ve.direct Settings", + "PowerMeterSettings": "Power Meter Settings", "BatterySettings": "@:batteryadmin.BatterySettings", "ConfigManagement": "Config Management", "FirmwareUpgrade": "Firmware Upgrade", @@ -452,6 +453,24 @@ "UpdatesOnly": "Send only updates:", "Save": "@:dtuadmin.Save" }, + "powermeteradmin":{ + "PowerMeterSettings": "Power Meter Settings", + "PowerMeterConfiguration": "Power Meter Configuration", + "PowerMeterEnable": "Enable Power Meter", + "PowerMeterParameter": "Power Meter Parameter", + "PowerMeterSource": "Power Meter type", + "MQTT": "MQTT Parameter", + "typeMQTT": "MQTT", + "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", + "typeSDM3ph": "SDM 3 phase (SDM72/630)", + "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", + "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", + "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", + "SDM": "SDM-Power Meter Parameter", + "sdmbaudrate": "Baudrate", + "sdmaddress": "Modbus Address", + "Save": "@:dtuadmin.Save" + }, "powerlimiteradmin": { "PowerLimiterSettings": "Power Limiter Settings", "PowerLimiterConfiguration": "Power Limiter Configuration", diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index a6b3ee60c..c9577fd9e 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -7,6 +7,7 @@ import DtuAdminView from '@/views/DtuAdminView.vue'; import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'; import HomeView from '@/views/HomeView.vue'; import VedirectAdminView from '@/views/VedirectAdminView.vue' +import PowerMeterAdminView from '@/views/PowerMeterAdminView.vue' import PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue' import VedirectInfoView from '@/views/VedirectInfoView.vue' import InverterAdminView from '@/views/InverterAdminView.vue'; @@ -86,6 +87,11 @@ const router = createRouter({ name: 'Ve.direct Settings', component: VedirectAdminView }, + { + path: '/settings/powermeter', + name: 'Power meter Settings', + component: PowerMeterAdminView + }, { path: '/settings/powerlimiter', name: 'Power limiter Settings', diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index 09521dfe0..bbc01d3b9 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -2,9 +2,6 @@ export interface PowerLimiterConfig { enabled: boolean; solar_passtrough_enabled: boolean; battery_drain_strategy: number; - mqtt_topic_powermeter_1: string; - mqtt_topic_powermeter_2: string; - mqtt_topic_powermeter_3: string; is_inverter_behind_powermeter: boolean; inverter_id: number; inverter_channel_id: number; diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts new file mode 100644 index 000000000..325e51175 --- /dev/null +++ b/webapp/src/types/PowerMeterConfig.ts @@ -0,0 +1,10 @@ +export interface PowerMeterConfig { + enabled: boolean; + source: number; + interval: number; + mqtt_topic_powermeter_1: string; + mqtt_topic_powermeter_2: string; + mqtt_topic_powermeter_3: string; + sdmbaudrate: number; + sdmaddress: number; +} diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 2c00301d0..bf7082279 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -114,36 +114,6 @@ -
    - -
    -
    - -
    -
    -
    - -
    - -
    -
    - -
    -
    -
    - -
    - -
    -
    - -
    -
    -
    -
    diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue new file mode 100644 index 000000000..815c54051 --- /dev/null +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -0,0 +1,160 @@ + + + diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz index 0830730b7..f7547917e 100644 Binary files a/webapp_dist/index.html.gz and b/webapp_dist/index.html.gz differ diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index dffc6d508..28a80d87e 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index f1ea50bf1..45374b962 100644 Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ