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:
parent
09942e8e18
commit
f560f25302
19
README.md
19
README.md
@ -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
|
||||
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
||||
[](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:
|
||||

|
||||
|
||||
|
||||
43
include/Battery.h
Normal file
43
include/Battery.h
Normal 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;
|
||||
@ -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
34
include/PowerLimiter.h
Normal 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;
|
||||
37
include/PylontechCanReceiver.h
Normal file
37
include/PylontechCanReceiver.h
Normal 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;
|
||||
@ -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
18
include/WebApi_battery.h
Normal 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;
|
||||
};
|
||||
18
include/WebApi_powerlimiter.h
Normal file
18
include/WebApi_powerlimiter.h
Normal 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;
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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
4
src/Battery.cpp
Normal file
@ -0,0 +1,4 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "Battery.h"
|
||||
|
||||
BatteryClass Battery;
|
||||
@ -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
269
src/PowerLimiter.cpp
Normal 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;
|
||||
}
|
||||
235
src/PylontechCanReceiver.cpp
Normal file
235
src/PylontechCanReceiver.cpp
Normal 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;
|
||||
}
|
||||
@ -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
113
src/WebApi_battery.cpp
Normal 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
132
src/WebApi_powerlimiter.cpp
Normal 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();
|
||||
}
|
||||
14
src/main.cpp
14
src/main.cpp
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
3
webapp/src/types/BatteryConfig.ts
Normal file
3
webapp/src/types/BatteryConfig.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface BatteryConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
15
webapp/src/types/PowerLimiterConfig.ts
Normal file
15
webapp/src/types/PowerLimiterConfig.ts
Normal 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;
|
||||
}
|
||||
79
webapp/src/views/BatteryAdminView.vue
Normal file
79
webapp/src/views/BatteryAdminView.vue
Normal 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>
|
||||
236
webapp/src/views/PowerLimiterAdminView.vue
Normal file
236
webapp/src/views/PowerLimiterAdminView.vue
Normal 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.
Loading…
Reference in New Issue
Block a user