initial merge of power_limiter * missing is inverter and channel setting in gui

* due to bug _webApiPrometheus.init is commented out
This commit is contained in:
helgeerbe 2023-02-20 15:56:02 +01:00
parent 09942e8e18
commit f560f25302
27 changed files with 1406 additions and 11 deletions

View File

@ -1,4 +1,18 @@
# OpenDTU_VeDirect
# OpenDTU-OnBattery
This is a fork from the Hoymiles project OpenDTU.
## Extensions to the original OpenDTU
This project is still under development and adds following features:
* Support Victron's Ve.Direct protocol on the same chip. Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household (needs an MQTT based power meter like Shelly 3EM)
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
* Voltage correction that takes the voltage drop because of the current output load into account (not 100% reliable calculation)
* Can read the current solar panel power from the Victron MPPT and adjust the limiter accordingly to not save energy in the battery (for increased system efficiency). Increases the battery lifespan and reduces energy loses.
* Settings can be configured in the UI
* Pylontech Battery support (via CAN bus interface). Use the SOC for starting/stopping the power output and provide the battery data via MQTT
[![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
[![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
@ -14,9 +28,6 @@ Several screenshots of the frontend can be found here: [Screenshots](docs/screen
Different builds from existing installations can be found here [Builds](docs/builds/README.md)
Like to show your own build? Just send me a Pull Request.
## Extensions to the original OpenDTU
I extended the original OpenDTU software to support also Victron's Ve.Direct protocol on the same chip. Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
### Web-Live-Interface:
![image](https://user-images.githubusercontent.com/59169507/187224107-4e0d0cab-2e1b-4e47-9410-a49f80aa6789.png)

43
include/Battery.h Normal file
View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
class BatteryClass {
public:
float chargeVoltage;
float chargeCurrentLimitation;
float dischargeCurrentLimitation;
uint16_t stateOfCharge;
uint32_t stateOfChargeLastUpdate;
uint16_t stateOfHealth;
float voltage;
float current;
float temperature;
bool alarmOverCurrentDischarge;
bool alarmUnderTemperature;
bool alarmOverTemperature;
bool alarmUnderVoltage;
bool alarmOverVoltage;
bool alarmBmsInternal;
bool alarmOverCurrentCharge;
bool warningHighCurrentDischarge;
bool warningLowTemperature;
bool warningHighTemperature;
bool warningLowVoltage;
bool warningHighVoltage;
bool warningBmsInternal;
bool warningHighCurrentCharge;
char manufacturer[9];
bool chargeEnabled;
bool dischargeEnabled;
bool chargeImmediately;
private:
};
extern BatteryClass Battery;

View File

@ -17,7 +17,7 @@
#define MQTT_MAX_HOSTNAME_STRLEN 128
#define MQTT_MAX_USERNAME_STRLEN 64
#define MQTT_MAX_PASSWORD_STRLEN 64
#define MQTT_MAX_TOPIC_STRLEN 32
#define MQTT_MAX_TOPIC_STRLEN 256
#define MQTT_MAX_LWTVALUE_STRLEN 20
#define MQTT_MAX_ROOT_CA_CERT_STRLEN 2560
@ -29,7 +29,7 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define JSON_BUFFER_SIZE 7000
#define JSON_BUFFER_SIZE 8192
struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower;
@ -93,6 +93,23 @@ struct CONFIG_T {
bool Mqtt_Hass_Expire;
bool PowerLimiter_Enabled;
bool PowerLimiter_SolarPassTroughEnabled;
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;
uint32_t PowerLimiter_LowerPowerLimit;
uint32_t PowerLimiter_UpperPowerLimit;
uint32_t PowerLimiter_BatterySocStartThreshold;
uint32_t PowerLimiter_BatterySocStopThreshold;
float PowerLimiter_VoltageStartThreshold;
float PowerLimiter_VoltageStopThreshold;
float PowerLimiter_VoltageLoadCorrectionFactor;
bool Battery_Enabled;
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
bool Security_AllowReadonly;

34
include/PowerLimiter.h Normal file
View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>
class PowerLimiterClass {
public:
void init();
void loop();
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;
uint16_t _lastRequestedPowerLimit;
bool _consumeSolarPowerOnly;
float _powerMeter1Power;
float _powerMeter2Power;
float _powerMeter3Power;
bool canUseDirectSolarPower();
uint32_t getDirectSolarPower();
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
bool isStopThresholdReached(std::shared_ptr<InverterAbstract> inverter);
};
extern PowerLimiterClass PowerLimiter;

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>
#ifndef PYLONTECH_PIN_RX
#define PYLONTECH_PIN_RX 27
#endif
#ifndef PYLONTECH_PIN_TX
#define PYLONTECH_PIN_TX 26
#endif
class PylontechCanReceiverClass {
public:
void init();
void loop();
void parseCanPackets();
void mqtt();
private:
uint8_t readUnsignedInt8();
uint16_t readUnsignedInt16();
int16_t readSignedInt16();
void readString(char* str, uint8_t numBytes);
void readBooleanBits8(bool* b, uint8_t numBits);
float scaleValue(int16_t value, float factor);
bool getBit(uint8_t value, uint8_t bit);
uint32_t _lastPublish;
};
extern PylontechCanReceiverClass PylontechCanReceiver;

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "WebApi_battery.h"
#include "WebApi_config.h"
#include "WebApi_devinfo.h"
#include "WebApi_dtu.h"
@ -14,6 +15,7 @@
#include "WebApi_network.h"
#include "WebApi_ntp.h"
#include "WebApi_power.h"
#include "WebApi_powerlimiter.h"
#include "WebApi_prometheus.h"
#include "WebApi_security.h"
#include "WebApi_sysstatus.h"
@ -37,6 +39,7 @@ private:
AsyncWebServer _server;
AsyncEventSource _events;
WebApiBatteryClass _webApiBattery;
WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo;
@ -50,6 +53,7 @@ private:
WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp;
WebApiPowerClass _webApiPower;
WebApiPowerLimiterClass _webApiPowerLimiter;
WebApiPrometheusClass _webApiPrometheus;
WebApiSecurityClass _webApiSecurity;
WebApiSysstatusClass _webApiSysstatus;

18
include/WebApi_battery.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiBatteryClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiPowerLimiterClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -88,4 +88,18 @@
#define VEDIRECT_ENABLED false
#define VEDIRECT_UPDATESONLY true
#define VEDIRECT_POLL_INTERVAL 5
#define VEDIRECT_POLL_INTERVAL 5
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTROUGH_ENABLED true
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
#define BATTERY_ENABLED false

View File

@ -27,6 +27,7 @@ lib_deps =
https://github.com/bertmelis/espMqttClient.git#v1.3.3
nrf24/RF24 @ ^1.4.5
olikraus/U8g2 @ ^2.34.13
https://github.com/berni2288/arduino-CAN
extra_scripts =
pre:auto_firmware_version.py
@ -148,4 +149,4 @@ build_flags = ${env.build_flags}
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5
-DHOYMILES_PIN_CS=5

4
src/Battery.cpp Normal file
View File

@ -0,0 +1,4 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Battery.h"
BatteryClass Battery;

View File

@ -109,6 +109,25 @@ bool ConfigurationClass::write()
vedirect["updates_only"] = config.Vedirect_UpdatesOnly;
vedirect["poll_interval"] = config.Vedirect_PollInterval;
JsonObject powerlimiter = doc.createNestedObject("powerlimiter");
powerlimiter["enabled"] = config.PowerLimiter_Enabled;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled;
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["lower_power_limit"] = config.PowerLimiter_LowerPowerLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter_UpperPowerLimit;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter_BatterySocStartThreshold;
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter_BatterySocStopThreshold;
powerlimiter["voltage_start_threshold"] = config.PowerLimiter_VoltageStartThreshold;
powerlimiter["voltage_stop_threshold"] = config.PowerLimiter_VoltageStopThreshold;
powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter_VoltageLoadCorrectionFactor;
JsonObject battery = doc.createNestedObject("battery");
battery["enabled"] = config.Battery_Enabled;
// Serialize JSON to file
if (serializeJson(doc, f) == 0) {
MessageOutput.println("Failed to write file");
@ -244,6 +263,25 @@ bool ConfigurationClass::read()
config.Vedirect_UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY;
config.Vedirect_PollInterval = vedirect["poll_interval"] | VEDIRECT_POLL_INTERVAL;
JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED;
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_LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
config.PowerLimiter_UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter_BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
config.PowerLimiter_BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
config.PowerLimiter_VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
config.PowerLimiter_VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
config.PowerLimiter_VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
JsonObject battery = doc["battery"];
config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED;
f.close();
return true;
}
@ -308,4 +346,4 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial)
return NULL;
}
ConfigurationClass Configuration;
ConfigurationClass Configuration;

269
src/PowerLimiter.cpp Normal file
View File

@ -0,0 +1,269 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "Battery.h"
#include "PowerLimiter.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include <VeDirectFrameHandler.h>
#include "MessageOutput.h"
#include <ctime>
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;
_lastRequestedPowerLimit = 0;
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));
}
_consumeSolarPowerOnly = false;
}
void PowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
MessageOutput.printf("PowerLimiterClass: Received MQTT message on topic: %s\r\n", topic);
CONFIG_T& config = Configuration.get();
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter1) == 0) {
_powerMeter1Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter2) == 0) {
_powerMeter2Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
if (strcmp(topic, config.PowerLimiter_MqttTopicPowerMeter3) == 0) {
_powerMeter3Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
}
_lastPowerMeterUpdate = millis();
}
void PowerLimiterClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter_Enabled
|| !MqttSettings.getConnected()
|| !Hoymiles.getRadio()->isIdle()
|| (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000)
|| (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) {
return;
}
_lastLoop = millis();
std::shared_ptr<InverterAbstract> inverter = Hoymiles.getInverterByPos(1); // TODO make Inverter selectable
if (inverter == nullptr || !inverter->isReachable()) {
return;
}
float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, CH0, FLD_UDC); // TODO make channel selectable
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000) {
return;
}
uint32_t victronChargePower = this->getDirectSolarPower();
MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d\r\n",
static_cast<int>(victronChargePower));
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) {
MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %f config.PowerLimiter_VoltageStartThreshold: %f config.PowerLimiter_VoltageStopThreshold: %f inverter->isProducing(): %d\r\n",
dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing());
}
if (inverter->isProducing()) {
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); // TODO check settings
float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
if ((_consumeSolarPowerOnly && isStartThresholdReached(inverter))
|| !canUseDirectSolarPower()) {
// The battery is full enough again, use the full battery power from now on.
_consumeSolarPowerOnly = false;
} else if (!_consumeSolarPowerOnly && isStopThresholdReached(inverter) && canUseDirectSolarPower()) {
// The battery voltage dropped too low
_consumeSolarPowerOnly = true;
}
if ((!_consumeSolarPowerOnly && isStopThresholdReached(inverter))
|| (_consumeSolarPowerOnly && victronChargePower < 10)) {
// DC voltage too low, stop the inverter
MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %f Corrected DC voltage: %f...\r\n",
dcVoltage, correctedDcVoltage);
MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
inverter->sendPowerControlRequest(Hoymiles.getRadio(), false);
uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = newPowerLimit;
_lastCommandSent = millis();
_consumeSolarPowerOnly = false;
return;
}
} else {
if ((isStartThresholdReached(inverter)) || victronChargePower >= 20) {
// DC voltage high enough, start the inverter
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
_lastCommandSent = millis();
inverter->sendPowerControlRequest(Hoymiles.getRadio(), true);
// In this mode, the inverter should consume the current solar power only
// and not drain additional power from the battery
if (!isStartThresholdReached(inverter)) {
_consumeSolarPowerOnly = true;
}
}
return;
}
int32_t newPowerLimit = 0;
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) {
newPowerLimit = static_cast<int>(_powerMeter1Power + _powerMeter2Power + _powerMeter3Power);
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.
// We don't use FLD_PAC from the statistics, because that
// data might be too old and unrelieable.
newPowerLimit += _lastRequestedPowerLimit;
}
newPowerLimit -= 10;
uint16_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit;
if (_consumeSolarPowerOnly && upperPowerLimit > victronChargePower) {
// Battery voltage too low, use Victron solar power only
upperPowerLimit = victronChargePower;
}
newPowerLimit = constrain(newPowerLimit, (uint16_t)config.PowerLimiter_LowerPowerLimit, upperPowerLimit);
MessageOutput.printf("[PowerLimiterClass::loop] powerMeter: %d W lastRequestedPowerLimit: %d\r\n",
static_cast<int>(_powerMeter1Power + _powerMeter2Power + _powerMeter3Power), _lastRequestedPowerLimit);
} else {
// If the power meter values are older than 30 seconds,
// set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons.
newPowerLimit = config.PowerLimiter_LowerPowerLimit;
}
MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = newPowerLimit;
_lastCommandSent = millis();
}
bool PowerLimiterClass::canUseDirectSolarPower()
{
CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter_SolarPassTroughEnabled
|| !config.Vedirect_Enabled
|| !VeDirect.veMap.count("PPV")) {
return false;
}
if (VeDirect.veMap["PPV"].toInt() < 10) {
// Not enough power
return false;
}
return true;
}
uint32_t PowerLimiterClass::getDirectSolarPower()
{
if (!this->canUseDirectSolarPower()) {
return 0;
}
return VeDirect.veMap["PPV"].toInt();
}
float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter)
{
CONFIG_T& config = Configuration.get();
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); // TODO check settings
float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, CH0, FLD_UDC); // TODO check settings
if (dcVoltage <= 0.0) {
return 0.0;
}
return dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
}
bool PowerLimiterClass::isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter)
{
CONFIG_T& config = Configuration.get();
// If the Battery interface is enabled, use the SOC value
if (config.Battery_Enabled
&& config.PowerLimiter_BatterySocStartThreshold > 0.0
&& (millis() - Battery.stateOfChargeLastUpdate) < 60000
&& Battery.stateOfCharge >= config.PowerLimiter_BatterySocStartThreshold) {
return true;
}
// Otherwise we use the voltage threshold
if (config.PowerLimiter_VoltageStartThreshold <= 0.0) {
return false;
}
float correctedDcVoltage = getLoadCorrectedVoltage(inverter);
return correctedDcVoltage >= config.PowerLimiter_VoltageStartThreshold;
}
bool PowerLimiterClass::isStopThresholdReached(std::shared_ptr<InverterAbstract> inverter)
{
CONFIG_T& config = Configuration.get();
// If the Battery interface is enabled, use the SOC value
if (config.Battery_Enabled
&& config.PowerLimiter_BatterySocStopThreshold > 0.0
&& (millis() - Battery.stateOfChargeLastUpdate) < 60000
&& Battery.stateOfCharge <= config.PowerLimiter_BatterySocStopThreshold) {
return true;
}
// Otherwise we use the voltage threshold
if (config.PowerLimiter_VoltageStopThreshold <= 0.0) {
return false;
}
float correctedDcVoltage = getLoadCorrectedVoltage(inverter);
return correctedDcVoltage <= config.PowerLimiter_VoltageStopThreshold;
}

View File

@ -0,0 +1,235 @@
#include "PylontechCanReceiver.h"
#include "Battery.h"
#include "Configuration.h"
#include "MqttSettings.h"
#include <CAN.h>
#include <ctime>
//#define PYLONTECH_DEBUG_ENABLED
PylontechCanReceiverClass PylontechCanReceiver;
void PylontechCanReceiverClass::init()
{
CONFIG_T& config = Configuration.get();
if (!config.Battery_Enabled) {
return;
}
CAN.setPins(PYLONTECH_PIN_RX, PYLONTECH_PIN_TX);
if (!CAN.begin(500E3)) {
Hoymiles.getMessageOutput()->println("Starting CAN failed!");
}
}
void PylontechCanReceiverClass::loop()
{
CONFIG_T& config = Configuration.get();
if (!config.Battery_Enabled) {
return;
}
parseCanPackets();
mqtt();
}
void PylontechCanReceiverClass::mqtt()
{
CONFIG_T& config = Configuration.get();
if (!MqttSettings.getConnected()
|| (millis() - _lastPublish) < (config.Mqtt_PublishInterval * 1000)) {
return;
}
_lastPublish = millis();
String topic = "battery";
MqttSettings.publish(topic + "/settings/chargeVoltage", String(Battery.chargeVoltage));
MqttSettings.publish(topic + "/settings/chargeCurrentLimitation", String(Battery.chargeCurrentLimitation));
MqttSettings.publish(topic + "/settings/dischargeCurrentLimitation", String(Battery.dischargeCurrentLimitation));
MqttSettings.publish(topic + "/stateOfCharge", String(Battery.stateOfCharge));
MqttSettings.publish(topic + "/stateOfHealth", String(Battery.stateOfHealth));
MqttSettings.publish(topic + "/voltage", String(Battery.voltage));
MqttSettings.publish(topic + "/current", String(Battery.current));
MqttSettings.publish(topic + "/temperature", String(Battery.temperature));
MqttSettings.publish(topic + "/alarm/overCurrentDischarge", String(Battery.alarmOverCurrentDischarge));
MqttSettings.publish(topic + "/alarm/underTemperature", String(Battery.alarmUnderTemperature));
MqttSettings.publish(topic + "/alarm/overTemperature", String(Battery.alarmOverTemperature));
MqttSettings.publish(topic + "/alarm/underVoltage", String(Battery.alarmUnderVoltage));
MqttSettings.publish(topic + "/alarm/overVoltage", String(Battery.alarmOverVoltage));
MqttSettings.publish(topic + "/alarm/bmsInternal", String(Battery.alarmBmsInternal));
MqttSettings.publish(topic + "/alarm/overCurrentCharge", String(Battery.alarmOverCurrentCharge));
MqttSettings.publish(topic + "/warning/highCurrentDischarge", String(Battery.warningHighCurrentDischarge));
MqttSettings.publish(topic + "/warning/lowTemperature", String(Battery.warningLowTemperature));
MqttSettings.publish(topic + "/warning/highTemperature", String(Battery.warningHighTemperature));
MqttSettings.publish(topic + "/warning/lowVoltage", String(Battery.warningLowVoltage));
MqttSettings.publish(topic + "/warning/highVoltage", String(Battery.warningHighVoltage));
MqttSettings.publish(topic + "/warning/bmsInternal", String(Battery.warningBmsInternal));
MqttSettings.publish(topic + "/warning/highCurrentCharge", String(Battery.warningHighCurrentCharge));
MqttSettings.publish(topic + "/manufacturer", Battery.manufacturer);
MqttSettings.publish(topic + "/charging/chargeEnabled", String(Battery.chargeEnabled));
MqttSettings.publish(topic + "/charging/dischargeEnabled", String(Battery.dischargeEnabled));
MqttSettings.publish(topic + "/charging/chargeImmediately", String(Battery.chargeImmediately));
}
void PylontechCanReceiverClass::parseCanPackets()
{
// try to parse packet
int packetSize = CAN.parsePacket();
if ((packetSize <= 0 && CAN.packetId() == -1)
|| CAN.packetRtr()) {
return;
}
switch (CAN.packetId()) {
case 0x351: {
Battery.chargeVoltage = this->scaleValue(this->readUnsignedInt16(), 0.1);
Battery.chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(), 0.1);
Battery.dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(), 0.1);
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\n",
Battery.chargeVoltage, Battery.chargeCurrentLimitation, Battery.dischargeCurrentLimitation);
#endif
break;
}
case 0x355: {
Battery.stateOfCharge = this->readUnsignedInt16();
Battery.stateOfChargeLastUpdate = millis();
Battery.stateOfHealth = this->readUnsignedInt16();
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] soc: %d soh: %d\n",
Battery.stateOfCharge, Battery.stateOfHealth);
#endif
break;
}
case 0x356: {
Battery.voltage = this->scaleValue(this->readSignedInt16(), 0.01);
Battery.current = this->scaleValue(this->readSignedInt16(), 0.1);
Battery.temperature = this->scaleValue(this->readSignedInt16(), 0.1);
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] voltage: %f current: %f temperature: %f\n",
Battery.voltage, Battery.current, Battery.temperature);
#endif
break;
}
case 0x359: {
uint16_t alarmBits = this->readUnsignedInt8();
Battery.alarmOverCurrentDischarge = this->getBit(alarmBits, 7);
Battery.alarmUnderTemperature = this->getBit(alarmBits, 4);
Battery.alarmOverTemperature = this->getBit(alarmBits, 3);
Battery.alarmUnderVoltage = this->getBit(alarmBits, 2);
Battery.alarmOverVoltage= this->getBit(alarmBits, 1);
alarmBits = this->readUnsignedInt8();
Battery.alarmBmsInternal= this->getBit(alarmBits, 3);
Battery.alarmOverCurrentCharge = this->getBit(alarmBits, 0);
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] Alarms: %d %d %d %d %d %d %d\n",
Battery.alarmOverCurrentDischarge,
Battery.alarmUnderTemperature,
Battery.alarmOverTemperature,
Battery.alarmUnderVoltage,
Battery.alarmOverVoltage,
Battery.alarmBmsInternal,
Battery.alarmOverCurrentCharge);
#endif
uint16_t warningBits = this->readUnsignedInt8();
Battery.warningHighCurrentDischarge = this->getBit(warningBits, 7);
Battery.warningLowTemperature = this->getBit(warningBits, 4);
Battery.warningHighTemperature = this->getBit(warningBits, 3);
Battery.warningLowVoltage = this->getBit(warningBits, 2);
Battery.warningHighVoltage = this->getBit(warningBits, 1);
warningBits = this->readUnsignedInt8();
Battery.warningBmsInternal= this->getBit(warningBits, 3);
Battery.warningHighCurrentCharge = this->getBit(warningBits, 0);
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] Warnings: %d %d %d %d %d %d %d\n",
Battery.warningHighCurrentDischarge,
Battery.warningLowTemperature,
Battery.warningHighTemperature,
Battery.warningLowVoltage,
Battery.warningHighVoltage,
Battery.warningBmsInternal,
Battery.warningHighCurrentCharge);
#endif
break;
}
case 0x35E: {
String manufacturer = CAN.readString();
if (manufacturer == "") {
break;
}
strlcpy(Battery.manufacturer, manufacturer.c_str(), sizeof(Battery.manufacturer));
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] Manufacturer: %s\n", manufacturer.c_str());
#endif
break;
}
case 0x35C: {
uint16_t chargeStatusBits = this->readUnsignedInt8();
Battery.chargeEnabled = this->getBit(chargeStatusBits, 7);
Battery.dischargeEnabled = this->getBit(chargeStatusBits, 6);
Battery.chargeImmediately = this->getBit(chargeStatusBits, 5);
#ifdef PYLONTECH_DEBUG_ENABLED
Hoymiles.getMessageOutput()->printf("[Pylontech] chargeStatusBits: %d %d %d\n",
Battery.chargeEnabled,
Battery.dischargeEnabled,
Battery.chargeImmediately);
#endif
this->readUnsignedInt8();
break;
}
}
}
uint8_t PylontechCanReceiverClass::readUnsignedInt8()
{
return CAN.read();
}
uint16_t PylontechCanReceiverClass::readUnsignedInt16()
{
uint8_t bytes[2];
bytes[0] = (uint8_t)CAN.read();
bytes[1] = (uint8_t)CAN.read();
return (bytes[1] << 8) + bytes[0];
}
int16_t PylontechCanReceiverClass::readSignedInt16()
{
return this->readUnsignedInt16();
}
float PylontechCanReceiverClass::scaleValue(int16_t value, float factor)
{
return value * factor;
}
bool PylontechCanReceiverClass::getBit(uint8_t value, uint8_t bit)
{
return (value & (1 << bit)) >> bit;
}

View File

@ -17,6 +17,7 @@ void WebApiClass::init()
{
_server.addHandler(&_events);
_webApiBattery.init(&_server);
_webApiConfig.init(&_server);
_webApiDevice.init(&_server);
_webApiDevInfo.init(&_server);
@ -30,7 +31,8 @@ void WebApiClass::init()
_webApiNetwork.init(&_server);
_webApiNtp.init(&_server);
_webApiPower.init(&_server);
_webApiPrometheus.init(&_server);
_webApiPowerLimiter.init(&_server);
// _webApiPrometheus.init(&_server); // TODO remove
_webApiSecurity.init(&_server);
_webApiSysstatus.init(&_server);
_webApiWebapp.init(&_server);
@ -44,6 +46,7 @@ void WebApiClass::init()
void WebApiClass::loop()
{
_webApiBattery.loop();
_webApiConfig.loop();
_webApiDevice.loop();
_webApiDevInfo.loop();
@ -57,6 +60,7 @@ void WebApiClass::loop()
_webApiNetwork.loop();
_webApiNtp.loop();
_webApiPower.loop();
_webApiPowerLimiter.loop();
_webApiSecurity.loop();
_webApiSysstatus.loop();
_webApiWebapp.loop();

113
src/WebApi_battery.cpp Normal file
View File

@ -0,0 +1,113 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Battery.h"
#include "Configuration.h"
#include "PylontechCanReceiver.h"
#include "WebApi.h"
#include "WebApi_battery.h"
#include "WebApi_errors.h"
#include "helper.h"
void WebApiBatteryClass::init(AsyncWebServer* server)
{
using std::placeholders::_1;
_server = server;
_server->on("/api/battery/status", HTTP_GET, std::bind(&WebApiBatteryClass::onStatus, this, _1));
_server->on("/api/battery/config", HTTP_GET, std::bind(&WebApiBatteryClass::onAdminGet, this, _1));
_server->on("/api/battery/config", HTTP_POST, std::bind(&WebApiBatteryClass::onAdminPost, this, _1));
}
void WebApiBatteryClass::loop()
{
}
void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentialsReadonly(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.Battery_Enabled;
response->setLength();
request->send(response);
}
void WebApiBatteryClass::onAdminGet(AsyncWebServerRequest* request)
{
onStatus(request);
}
void WebApiBatteryClass::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!");
retMsg[F("code")] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg[F("message")] = F("Data too large!");
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg[F("message")] = F("Failed to parse data!");
retMsg[F("code")] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("enabled"))) {
retMsg[F("message")] = F("Values are missing!");
retMsg[F("code")] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
return;
}
CONFIG_T& config = Configuration.get();
config.Battery_Enabled = root[F("enabled")].as<bool>();
Configuration.write();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
retMsg[F("code")] = WebApiError::GenericSuccess;
response->setLength();
request->send(response);
if (config.Battery_Enabled) {
PylontechCanReceiver.init();
}
}

132
src/WebApi_powerlimiter.cpp Normal file
View File

@ -0,0 +1,132 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "WebApi_powerlimiter.h"
#include "VeDirectFrameHandler.h"
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Configuration.h"
#include "MqttHandleHass.h"
#include "MqttSettings.h"
#include "PowerLimiter.h"
#include "WebApi.h"
#include "helper.h"
void WebApiPowerLimiterClass::init(AsyncWebServer* server)
{
using std::placeholders::_1;
_server = server;
_server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1));
_server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1));
_server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1));
}
void WebApiPowerLimiterClass::loop()
{
}
void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.PowerLimiter_Enabled;
root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled;
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("lower_power_limit")] = config.PowerLimiter_LowerPowerLimit;
root[F("upper_power_limit")] = config.PowerLimiter_UpperPowerLimit;
root[F("battery_soc_start_threshold")] = config.PowerLimiter_BatterySocStartThreshold;
root[F("battery_soc_stop_threshold")] = config.PowerLimiter_BatterySocStopThreshold;
root[F("voltage_start_threshold")] = config.PowerLimiter_VoltageStartThreshold;
root[F("voltage_stop_threshold")] = config.PowerLimiter_VoltageStopThreshold;
root[F("voltage_load_correction_factor")] = config.PowerLimiter_VoltageLoadCorrectionFactor;
response->setLength();
request->send(response);
}
void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
this->onStatus(request);
}
void WebApiPowerLimiterClass::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("lower_power_limit"))) {
retMsg[F("message")] = F("Values are missing!");
response->setLength();
request->send(response);
return;
}
CONFIG_T& config = Configuration.get();
config.PowerLimiter_Enabled = root[F("enabled")].as<bool>();
config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as<bool>();
strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter1));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter2));
strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as<String>().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter3));
config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>();
config.PowerLimiter_LowerPowerLimit = root[F("lower_power_limit")].as<uint32_t>();
config.PowerLimiter_UpperPowerLimit = root[F("upper_power_limit")].as<uint32_t>();
config.PowerLimiter_BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as<float>();
config.PowerLimiter_BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as<float>();
config.PowerLimiter_VoltageStartThreshold = root[F("voltage_start_threshold")].as<float>();
config.PowerLimiter_VoltageStopThreshold = root[F("voltage_stop_threshold")].as<float>();
config.PowerLimiter_VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as<float>();
Configuration.write();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
response->setLength();
request->send(response);
MqttSettings.performReconnect();
MqttHandleHass.forceUpdate();
PowerLimiter.init();
}

View File

@ -17,6 +17,8 @@
#include "PinMapping.h"
#include "Utils.h"
#include "WebApi.h"
#include "PowerLimiter.h"
#include "PylontechCanReceiver.h"
#include "defaults.h"
#include <Arduino.h>
#include <Hoymiles.h>
@ -175,12 +177,20 @@ void setup()
} else {
MessageOutput.println(F("Invalid pin config"));
}
// Dynamic power limiter
PowerLimiter.init();
// Pylontech / CAN bus
PylontechCanReceiver.init();
}
void loop()
{
NetworkSettings.loop();
yield();
PowerLimiter.loop();
yield();
Hoymiles.loop();
yield();
// Vedirect_Enabled is unknown to lib. Therefor check has to be done here
@ -204,4 +214,6 @@ void loop()
yield();
MessageOutput.loop();
yield();
}
PylontechCanReceiver.loop();
yield();
}

View File

@ -51,6 +51,12 @@
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/powerlimiter">Dynamic Power Limiter</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/battery">{{ $t('menu.BatterySettings') }}</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{ $t('menu.DeviceManager') }}</router-link>
</li>

View File

@ -10,6 +10,7 @@
"DTUSettings": "DTU Einstellungen",
"DeviceManager": "Geräte-Manager",
"VedirectSettings": "Ve.direct Settings",
"BatterySettings": "Battery Settings",
"ConfigManagement": "Konfigurationsverwaltung",
"FirmwareUpgrade": "Firmware Aktualisierung",
"DeviceReboot": "Geräteneustart",
@ -434,6 +435,13 @@
"UpdatesOnly": "Nur Änderungen senden:",
"Save": "@:dtuadmin.Save"
},
"batteryadmin": {
"BatterySettings": "Batterie Einstellungen",
"BatteryConfiguration": "Batterie Konfiguration",
"EnableBatteryCanBus": "Aktiviere Batterie CAN Bus Schnittstelle",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
},
"inverteradmin": {
"InverterSettings": "Wechselrichter Einstellungen",
"AddInverter": "Neuen Wechselrichter hinzufügen",

View File

@ -10,6 +10,7 @@
"DTUSettings": "DTU Settings",
"DeviceManager": "Device-Manager",
"VedirectSettings": "Ve.direct Settings",
"BatterySettings": "@:batteryadmin.BatterySettings",
"ConfigManagement": "Config Management",
"FirmwareUpgrade": "Firmware Upgrade",
"DeviceReboot": "Device Reboot",
@ -434,6 +435,37 @@
"UpdatesOnly": "Send only updates:",
"Save": "@:dtuadmin.Save"
},
"powerlimiteradmin": {
"PowerLimiterSettings": "Power Limiter Settings",
"PowerLimiterConfiguration": "Power Limiter Configuration",
"General": "General",
"Enable": "Enable",
"EnableSolarPasstrough": "Enable Solar Passtrough",
"SolarpasstroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.",
"LowerPowerLimit": "Lower power limit / continuous feed",
"UpperPowerLimit": "Upper power limit",
"PowerMeters": "Power meters - MQTT",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
"BatterySocStartThreshold": "Battery SOC - Start threshold",
"BatterySocStopThreshold": "Battery SOC - Stop threshold",
"VoltageStartThreshold": "DC Voltage - Start threshold",
"VoltageStopThreshold": "DC Voltage - Stop threshold",
"VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor",
"BatterySocInfo": "<b>Hint:</b> The battery SOC (State of charge) values can only be used when the Battery CAN Bus interface is enabled. If the battery has not reported any updates of SOC in the last minute, the voltage thresholds will be used as fallback.",
"InverterIsBehindPowerMeter": "Inverter is behind Power meter",
"Battery": "DC / Battery",
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).",
"Save": "@:dtuadmin.Save"
},
"batteryadmin": {
"BatterySettings": "Battery Settings",
"BatteryConfiguration": "Battery Configuration",
"EnableBatteryCanBus": "Enable Battery CAN Bus Interface",
"Seconds": "@:dtuadmin.Seconds",
"Save": "@:dtuadmin.Save"
},
"inverteradmin": {
"InverterSettings": "Inverter Settings",
"AddInverter": "Add a new Inverter",

View File

@ -1,4 +1,5 @@
import AboutView from '@/views/AboutView.vue';
import BatteryAdminView from '@/views/BatteryAdminView.vue';
import ConfigAdminView from '@/views/ConfigAdminView.vue';
import ConsoleInfoView from '@/views/ConsoleInfoView.vue';
import DeviceAdminView from '@/views/DeviceAdminView.vue'
@ -6,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 PowerLimiterAdminView from '@/views/PowerLimiterAdminView.vue'
import VedirectInfoView from '@/views/VedirectInfoView.vue'
import InverterAdminView from '@/views/InverterAdminView.vue';
import LoginView from '@/views/LoginView.vue';
@ -84,6 +86,16 @@ const router = createRouter({
name: 'Ve.direct Settings',
component: VedirectAdminView
},
{
path: '/settings/powerlimiter',
name: 'Power limiter Settings',
component: PowerLimiterAdminView
},
{
path: '/settings/battery',
name: 'Battery Settings',
component: BatteryAdminView
},
{
path: '/settings/mqtt',
name: 'MqTT Settings',

View File

@ -0,0 +1,3 @@
export interface BatteryConfig {
enabled: boolean;
}

View File

@ -0,0 +1,15 @@
export interface PowerLimiterConfig {
enabled: boolean;
solar_passtrough_enabled: boolean;
mqtt_topic_powermeter_1: string;
mqtt_topic_powermeter_2: string;
mqtt_topic_powermeter_3: string;
is_inverter_behind_powermeter: boolean;
lower_power_limit: number;
upper_power_limit: number;
battery_soc_start_threshold: number;
battery_soc_stop_threshold: number;
voltage_start_threshold: number;
voltage_stop_threshold: number;
voltage_load_correction_factor: number;
}

View File

@ -0,0 +1,79 @@
<template>
<BasePage :title="$t('batteryadmin.BatterySettings')" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<form @submit="saveBatteryConfig">
<CardElement :text="$t('batteryadmin.BatteryConfiguration')" textVariant="text-bg-primary">
<InputElement :label="$t('batteryadmin.EnableBatteryCanBus')"
v-model="batteryConfigList.enabled"
type="checkbox" wide/>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('batteryadmin.Save') }}</button>
</form>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import InputElement from '@/components/InputElement.vue';
import type { BatteryConfig } from "@/types/BatteryConfig";
import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
CardElement,
InputElement,
},
data() {
return {
dataLoading: true,
batteryConfigList: {} as BatteryConfig,
alertMessage: "",
alertType: "info",
showAlert: false,
};
},
created() {
this.getBatteryConfig();
},
methods: {
getBatteryConfig() {
this.dataLoading = true;
fetch("/api/battery/config", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.batteryConfigList = data;
this.dataLoading = false;
});
},
saveBatteryConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(this.batteryConfigList));
fetch("/api/battery/config", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
this.alertType = response.type;
this.showAlert = true;
}
);
},
},
});
</script>

View File

@ -0,0 +1,236 @@
<template>
<BasePage :title="'Dynamic Power limiter Settings'" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<form @submit="savePowerLimiterConfig">
<div class="card">
<div class="card-header text-bg-primary">{{ $t('powerlimiteradmin.General') }}</div>
<div class="card-body">
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputPowerlimiter">{{ $t('powerlimiteradmin.Enable') }}</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputPowerlimiter"
v-model="powerLimiterConfigList.enabled" />
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="solarPasstroughEnabled">{{ $t('powerlimiteradmin.EnableSolarPasstrough') }}</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="solarPasstroughEnabled"
v-model="powerLimiterConfigList.solar_passtrough_enabled" />
</div>
</div>
</div>
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.SolarpasstroughInfo')"></div>
<div class="row mb-3">
<label for="inputLowerPowerLimit" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.LowerPowerLimit') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" class="form-control" id="inputLowerPowerLimit"
placeholder="50" min="10" v-model="powerLimiterConfigList.lower_power_limit"
aria-describedby="lowerPowerLimitDescription" />
<span class="input-group-text" id="lowerPowerLimitDescription">W</span>
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputUpperPowerLimit" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.UpperPowerLimit') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" class="form-control" id="inputUpperPowerLimit"
placeholder="800" v-model="powerLimiterConfigList.upper_power_limit"
aria-describedby="upperPowerLimitDescription" />
<span class="input-group-text" id="upperPowerLimitDescription">W</span>
</div>
</div>
</div>
</div>
</div>
<div class="card mt-5">
<div class="card-header text-bg-primary">{{ $t('powerlimiteradmin.PowerMeters') }}</div>
<div class="card-body">
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter1" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter1') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter1"
placeholder="shellies/shellyem3/emeter/0/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_1" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter2" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter2') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter2"
placeholder="shellies/shellyem3/emeter/1/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_2" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter3" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.MqttTopicPowerMeter3') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter3"
placeholder="shellies/shellyem3/emeter/2/power" v-model="powerLimiterConfigList.mqtt_topic_powermeter_3" />
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputRetain">{{ $t('powerlimiteradmin.InverterIsBehindPowerMeter') }}</label>
<div class="col-sm-10">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="inputIsInverterBehindPowerMeter"
v-model="powerLimiterConfigList.is_inverter_behind_powermeter" />
</div>
</div>
</div>
</div>
</div>
<div class="card mt-5">
<div class="card-header text-bg-primary">{{ $t('powerlimiteradmin.Battery') }}</div>
<div class="card-body">
<div class="row mb-3">
<label for="batterySocStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.BatterySocStartThreshold') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" class="form-control" id="batterySocStartThreshold"
placeholder="80" v-model="powerLimiterConfigList.battery_soc_start_threshold"
aria-describedby="batterySocStartThresholdDescription" min="0" max="100" />
<span class="input-group-text" id="batterySocStartThresholdDescription">%</span>
</div>
</div>
</div>
<div class="row mb-3">
<label for="batterySocStopThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.BatterySocStopThreshold') }}</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" class="form-control" id="batterySocStopThreshold"
placeholder="20" v-model="powerLimiterConfigList.battery_soc_stop_threshold"
aria-describedby="batterySocStopThresholdDescription" min="0" max="100" />
<span class="input-group-text" id="batterySocStopThresholdDescription">%</span>
</div>
</div>
</div>
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.BatterySocInfo')"></div>
<div class="row mb-3">
<label for="inputVoltageStartThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageStartThreshold') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" step="0.01" class="form-control" id="inputVoltageStartThreshold"
placeholder="50" v-model="powerLimiterConfigList.voltage_start_threshold"
aria-describedby="voltageStartThresholdDescription" />
<span class="input-group-text" id="voltageStartThresholdDescription">V</span>
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputVoltageStopThreshold" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageStopThreshold') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" step="0.01" class="form-control" id="inputVoltageStopThreshold"
placeholder="49" v-model="powerLimiterConfigList.voltage_stop_threshold"
aria-describedby="voltageStopThresholdDescription" />
<span class="input-group-text" id="voltageStopThresholdDescription">V</span>
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputVoltageLoadCorrectionFactor" class="col-sm-2 col-form-label">{{ $t('powerlimiteradmin.VoltageLoadCorrectionFactor') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="number" step="0.0001" class="form-control" id="inputVoltageLoadCorrectionFactor"
placeholder="49" v-model="powerLimiterConfigList.voltage_load_correction_factor"
aria-describedby="voltageLoadCorrectionFactorDescription" />
<span class="input-group-text" id="voltageLoadCorrectionFactorDescription">V</span>
</div>
</div>
</div>
<div class="alert alert-secondary" role="alert" v-html="$t('powerlimiteradmin.VoltageLoadCorrectionInfo')"></div>
</div>
</div>
<button type="submit" class="btn btn-primary mb-3">{{ $t('powerlimiteradmin.Save') }}</button>
</form>
</BasePage>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig";
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
},
data() {
return {
dataLoading: true,
powerLimiterConfigList: {} as PowerLimiterConfig,
alertMessage: "",
alertType: "info",
showAlert: false,
};
},
created() {
this.getPowerLimiterConfig();
},
methods: {
getPowerLimiterConfig() {
this.dataLoading = true;
fetch("/api/powerlimiter/config", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.powerLimiterConfigList = data;
this.dataLoading = false;
});
},
savePowerLimiterConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(this.powerLimiterConfigList));
fetch("/api/powerlimiter/config", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
);
},
},
});
</script>

Binary file not shown.