@@ -137,6 +132,7 @@ import ModalDialog from '@/components/ModalDialog.vue';
import type { AlertResponse } from '@/types/Alert';
import type { FileInfo } from '@/types/File';
import { authHeader, handleResponse } from '@/utils/authentication';
+import { waitRestart } from '@/utils/waitRestart';
import * as bootstrap from 'bootstrap';
import {
BIconArrowLeft,
@@ -251,6 +247,7 @@ export default defineComponent({
// request.response will hold the response from the server
if (request.status === 200) {
this.UploadSuccess = true;
+ waitRestart(this.$router);
} else if (request.status !== 500) {
this.UploadError = `[HTTP ERROR] ${request.statusText}`;
} else {
diff --git a/webapp/src/views/FirmwareUpgradeView.vue b/webapp/src/views/FirmwareUpgradeView.vue
index 3828d00e9..15897c6f2 100644
--- a/webapp/src/views/FirmwareUpgradeView.vue
+++ b/webapp/src/views/FirmwareUpgradeView.vue
@@ -39,13 +39,6 @@
{{ $t('firmwareupgrade.OtaSuccess') }}
-
-
-
{
- // Check if the response status is OK (200-299 range)
- if (response.ok) {
- console.log('Remote host is available. Reloading page...');
- clearInterval(this.hostCheckInterval);
- this.hostCheckInterval = 0;
- // Perform a page reload
- window.location.replace('/');
- } else {
- console.log('Remote host is not reachable. Do something else if needed.');
- }
- })
- .catch((error) => {
- console.error('Error checking remote host:', error);
- });
- } else {
- console.log('Browser is offline. Cannot check remote host.');
- }
- },
},
mounted() {
if (!isLoggedIn()) {
@@ -229,8 +196,5 @@ export default defineComponent({
}
this.loading = false;
},
- unmounted() {
- clearInterval(this.hostCheckInterval);
- },
});
diff --git a/webapp/src/views/MaintenanceRebootView.vue b/webapp/src/views/MaintenanceRebootView.vue
index d80d050ca..682ce5006 100644
--- a/webapp/src/views/MaintenanceRebootView.vue
+++ b/webapp/src/views/MaintenanceRebootView.vue
@@ -36,6 +36,7 @@ import ModalDialog from '@/components/ModalDialog.vue';
import { authHeader, handleResponse, isLoggedIn } from '@/utils/authentication';
import * as bootstrap from 'bootstrap';
import { defineComponent } from 'vue';
+import { waitRestart } from '@/utils/waitRestart';
export default defineComponent({
components: {
@@ -80,6 +81,7 @@ export default defineComponent({
this.alertMessage = this.$t('apiresponse.' + data.code, data.param);
this.alertType = data.type;
this.showAlert = true;
+ waitRestart(this.$router);
});
this.onCloseModal(this.performReboot);
},
diff --git a/webapp/src/views/WaitRestartView.vue b/webapp/src/views/WaitRestartView.vue
new file mode 100644
index 000000000..95640e4b4
--- /dev/null
+++ b/webapp/src/views/WaitRestartView.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
From 2a21e53422296ca72e875c8603b8f8ecb9637b32 Mon Sep 17 00:00:00 2001
From: Thomas Basler
Date: Mon, 21 Oct 2024 22:41:52 +0200
Subject: [PATCH 017/130] webapp: Rename interface to prevent lint errors
---
webapp/src/types/{Alert.ts => AlertResponse.ts} | 0
webapp/src/views/ConfigAdminView.vue | 2 +-
webapp/src/views/InverterAdminView.vue | 2 +-
3 files changed, 2 insertions(+), 2 deletions(-)
rename webapp/src/types/{Alert.ts => AlertResponse.ts} (100%)
diff --git a/webapp/src/types/Alert.ts b/webapp/src/types/AlertResponse.ts
similarity index 100%
rename from webapp/src/types/Alert.ts
rename to webapp/src/types/AlertResponse.ts
diff --git a/webapp/src/views/ConfigAdminView.vue b/webapp/src/views/ConfigAdminView.vue
index ff90a656b..08ed31d81 100644
--- a/webapp/src/views/ConfigAdminView.vue
+++ b/webapp/src/views/ConfigAdminView.vue
@@ -129,7 +129,7 @@ import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from '@/components/BootstrapAlert.vue';
import CardElement from '@/components/CardElement.vue';
import ModalDialog from '@/components/ModalDialog.vue';
-import type { AlertResponse } from '@/types/Alert';
+import type { AlertResponse } from '@/types/AlertResponse';
import type { FileInfo } from '@/types/File';
import { authHeader, handleResponse } from '@/utils/authentication';
import { waitRestart } from '@/utils/waitRestart';
diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue
index d07f630d3..4ab82d34c 100644
--- a/webapp/src/views/InverterAdminView.vue
+++ b/webapp/src/views/InverterAdminView.vue
@@ -349,7 +349,7 @@ import CardElement from '@/components/CardElement.vue';
import InputElement from '@/components/InputElement.vue';
import InputSerial from '@/components/InputSerial.vue';
import ModalDialog from '@/components/ModalDialog.vue';
-import type { AlertResponse } from '@/types/Alert';
+import type { AlertResponse } from '@/types/AlertResponse';
import type { Inverter } from '@/types/InverterConfig';
import { authHeader, handleResponse } from '@/utils/authentication';
import * as bootstrap from 'bootstrap';
From b1edb13b3c32c0e8f0e0196eed14de1c0ee8c923 Mon Sep 17 00:00:00 2001
From: Bernhard Kirchen
Date: Thu, 17 Oct 2024 21:46:32 +0200
Subject: [PATCH 018/130] add and use configuration write guard
the configuration write guard is now required when the configuration
struct shall be mutated. the write guards locks multiple writers against
each other and also, more importantly, makes the writes synchronous to
the main loop. all code running in the main loop can now be sure that
(1) reads from the configuration struct are non-preemtive and (2) the
configuration struct as a whole is in a consistent state when reading
from it.
NOTE that acquiring a write guard from within the main loop's task will
immediately cause a deadlock and the watchdog will trigger a reset. if
writing from inside the main loop should ever become necessary, the
write guard must be updated to only lock the mutex but not wait for a
signal.
---
include/Configuration.h | 24 +++++++++++--
src/Configuration.cpp | 58 ++++++++++++++++++++++++++++--
src/WebApi.cpp | 4 +--
src/WebApi_device.cpp | 35 ++++++++++--------
src/WebApi_dtu.cpp | 20 ++++++-----
src/WebApi_inverter.cpp | 78 +++++++++++++++++++++++------------------
src/WebApi_mqtt.cpp | 64 +++++++++++++++++----------------
src/WebApi_network.cpp | 64 +++++++++++++++++----------------
src/WebApi_ntp.cpp | 18 ++++++----
src/WebApi_security.cpp | 10 ++++--
src/main.cpp | 17 ++-------
11 files changed, 243 insertions(+), 149 deletions(-)
diff --git a/include/Configuration.h b/include/Configuration.h
index 4a802e4e1..ce7bfbeea 100644
--- a/include/Configuration.h
+++ b/include/Configuration.h
@@ -3,6 +3,9 @@
#include "PinMapping.h"
#include
+#include
+#include
+#include
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change
@@ -161,15 +164,32 @@ struct CONFIG_T {
class ConfigurationClass {
public:
- void init();
+ void init(Scheduler& scheduler);
bool read();
bool write();
void migrate();
- CONFIG_T& get();
+ CONFIG_T const& get();
+
+ class WriteGuard {
+ public:
+ WriteGuard();
+ CONFIG_T& getConfig();
+ ~WriteGuard();
+
+ private:
+ std::unique_lock _lock;
+ };
+
+ WriteGuard getWriteGuard();
INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id);
+
+private:
+ void loop();
+
+ Task _loopTask;
};
extern ConfigurationClass Configuration;
diff --git a/src/Configuration.cpp b/src/Configuration.cpp
index db47d9c8a..34902e94f 100644
--- a/src/Configuration.cpp
+++ b/src/Configuration.cpp
@@ -13,8 +13,17 @@
CONFIG_T config;
-void ConfigurationClass::init()
+static std::condition_variable sWriterCv;
+static std::mutex sWriterMutex;
+static unsigned sWriterCount = 0;
+
+void ConfigurationClass::init(Scheduler& scheduler)
{
+ scheduler.addTask(_loopTask);
+ _loopTask.setCallback(std::bind(&ConfigurationClass::loop, this));
+ _loopTask.setIterations(TASK_FOREVER);
+ _loopTask.enable();
+
memset(&config, 0x0, sizeof(config));
}
@@ -318,6 +327,20 @@ bool ConfigurationClass::read()
}
f.close();
+
+ // Check for default DTU serial
+ MessageOutput.print("Check for default DTU serial... ");
+ if (config.Dtu.Serial == DTU_SERIAL) {
+ MessageOutput.print("generate serial based on ESP chip id: ");
+ const uint64_t dtuId = Utils::generateDtuSerial();
+ MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
+ ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
+ ((uint32_t)(dtuId & 0xFFFFFFFF)));
+ config.Dtu.Serial = dtuId;
+ write();
+ }
+ MessageOutput.println("done");
+
return true;
}
@@ -390,11 +413,16 @@ void ConfigurationClass::migrate()
read();
}
-CONFIG_T& ConfigurationClass::get()
+CONFIG_T const& ConfigurationClass::get()
{
return config;
}
+ConfigurationClass::WriteGuard ConfigurationClass::getWriteGuard()
+{
+ return WriteGuard();
+}
+
INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
{
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
@@ -439,4 +467,30 @@ void ConfigurationClass::deleteInverterById(const uint8_t id)
}
}
+void ConfigurationClass::loop()
+{
+ std::unique_lock lock(sWriterMutex);
+ if (sWriterCount == 0) { return; }
+
+ sWriterCv.notify_all();
+ sWriterCv.wait(lock, [] { return sWriterCount == 0; });
+}
+
+CONFIG_T& ConfigurationClass::WriteGuard::getConfig()
+{
+ return config;
+}
+
+ConfigurationClass::WriteGuard::WriteGuard()
+ : _lock(sWriterMutex)
+{
+ sWriterCount++;
+ sWriterCv.wait(_lock);
+}
+
+ConfigurationClass::WriteGuard::~WriteGuard() {
+ sWriterCount--;
+ if (sWriterCount == 0) { sWriterCv.notify_all(); }
+}
+
ConfigurationClass Configuration;
diff --git a/src/WebApi.cpp b/src/WebApi.cpp
index b3255c417..31eb122cc 100644
--- a/src/WebApi.cpp
+++ b/src/WebApi.cpp
@@ -47,7 +47,7 @@ void WebApiClass::reload()
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
{
- CONFIG_T& config = Configuration.get();
+ auto const& config = Configuration.get();
if (request->authenticate(AUTH_USERNAME, config.Security.Password)) {
return true;
}
@@ -65,7 +65,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request)
{
- CONFIG_T& config = Configuration.get();
+ auto const& config = Configuration.get();
if (config.Security.AllowReadonly) {
return true;
} else {
diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp
index 29686fe08..109ff1a99 100644
--- a/src/WebApi_device.cpp
+++ b/src/WebApi_device.cpp
@@ -129,23 +129,28 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
return;
}
- CONFIG_T& config = Configuration.get();
- bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping;
-
- strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as().c_str(), sizeof(config.Dev_PinMapping));
- config.Display.Rotation = root["display"]["rotation"].as();
- config.Display.PowerSafe = root["display"]["power_safe"].as();
- config.Display.ScreenSaver = root["display"]["screensaver"].as();
- config.Display.Contrast = root["display"]["contrast"].as();
- config.Display.Language = root["display"]["language"].as();
- config.Display.Diagram.Duration = root["display"]["diagramduration"].as();
- config.Display.Diagram.Mode = root["display"]["diagrammode"].as();
-
- for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
- config.Led_Single[i].Brightness = root["led"][i]["brightness"].as();
- config.Led_Single[i].Brightness = min(100, config.Led_Single[i].Brightness);
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as().c_str(), sizeof(config.Dev_PinMapping));
+ config.Display.Rotation = root["display"]["rotation"].as();
+ config.Display.PowerSafe = root["display"]["power_safe"].as();
+ config.Display.ScreenSaver = root["display"]["screensaver"].as();
+ config.Display.Contrast = root["display"]["contrast"].as();
+ config.Display.Language = root["display"]["language"].as();
+ config.Display.Diagram.Duration = root["display"]["diagramduration"].as();
+ config.Display.Diagram.Mode = root["display"]["diagrammode"].as();
+
+ for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
+ config.Led_Single[i].Brightness = root["led"][i]["brightness"].as();
+ config.Led_Single[i].Brightness = min(100, config.Led_Single[i].Brightness);
+ }
}
+ auto const& config = Configuration.get();
+ bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping;
+
Display.setDiagramMode(static_cast(config.Display.Diagram.Mode));
Display.setOrientation(config.Display.Rotation);
Display.enablePowerSafe = config.Display.PowerSafe;
diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp
index 7c6c3f738..e6fb8994c 100644
--- a/src/WebApi_dtu.cpp
+++ b/src/WebApi_dtu.cpp
@@ -27,7 +27,7 @@ void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler)
void WebApiDtuClass::applyDataTaskCb()
{
// Execute stuff in main thread to avoid busy SPI bus
- CONFIG_T& config = Configuration.get();
+ auto const& config = Configuration.get();
Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel);
Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel);
Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial);
@@ -153,14 +153,16 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
return;
}
- CONFIG_T& config = Configuration.get();
-
- config.Dtu.Serial = serial;
- config.Dtu.PollInterval = root["pollinterval"].as();
- config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as();
- config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as();
- config.Dtu.Cmt.Frequency = root["cmt_frequency"].as();
- config.Dtu.Cmt.CountryMode = root["cmt_country"].as();
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+ config.Dtu.Serial = serial;
+ config.Dtu.PollInterval = root["pollinterval"].as();
+ config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as();
+ config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as();
+ config.Dtu.Cmt.Frequency = root["cmt_frequency"].as();
+ config.Dtu.Cmt.CountryMode = root["cmt_country"].as();
+ }
WebApi.writeConfig(retMsg);
diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp
index ef353158f..4db9a57c9 100644
--- a/src/WebApi_inverter.cpp
+++ b/src/WebApi_inverter.cpp
@@ -184,9 +184,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
}
// Interpret the string as a hex value and convert it to uint64_t
- const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16);
+ const uint64_t new_serial = strtoll(root["serial"].as().c_str(), NULL, 16);
- if (serial == 0) {
+ if (new_serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@@ -209,37 +209,42 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
return;
}
- INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as()];
-
- uint64_t new_serial = serial;
- uint64_t old_serial = inverter.Serial;
-
- // Interpret the string as a hex value and convert it to uint64_t
- inverter.Serial = new_serial;
- strncpy(inverter.Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN);
-
- inverter.Poll_Enable = root["poll_enable"] | true;
- inverter.Poll_Enable_Night = root["poll_enable_night"] | true;
- inverter.Command_Enable = root["command_enable"] | true;
- inverter.Command_Enable_Night = root["command_enable_night"] | true;
- inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD;
- inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false;
- inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false;
- inverter.ClearEventlogOnMidnight = root["clear_eventlog"] | false;
- inverter.YieldDayCorrection = root["yieldday_correction"] | false;
-
- uint8_t arrayCount = 0;
- for (JsonVariant channel : channelArray) {
- inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as();
- inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as();
- strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name));
- arrayCount++;
+ uint64_t old_serial = 0;
+
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ INVERTER_CONFIG_T& inverter = config.Inverter[root["id"].as()];
+
+ old_serial = inverter.Serial;
+ inverter.Serial = new_serial;
+ strncpy(inverter.Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN);
+
+ inverter.Poll_Enable = root["poll_enable"] | true;
+ inverter.Poll_Enable_Night = root["poll_enable_night"] | true;
+ inverter.Command_Enable = root["command_enable"] | true;
+ inverter.Command_Enable_Night = root["command_enable_night"] | true;
+ inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD;
+ inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false;
+ inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false;
+ inverter.ClearEventlogOnMidnight = root["clear_eventlog"] | false;
+ inverter.YieldDayCorrection = root["yieldday_correction"] | false;
+
+ uint8_t arrayCount = 0;
+ for (JsonVariant channel : channelArray) {
+ inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as();
+ inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as();
+ strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name));
+ arrayCount++;
+ }
}
WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!");
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
+ INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[root["id"].as()];
std::shared_ptr inv = Hoymiles.getInverterBySerial(old_serial);
if (inv != nullptr && new_serial != old_serial) {
@@ -300,7 +305,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
}
uint8_t inverter_id = root["id"].as();
- INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id];
+ INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[inverter_id];
Hoymiles.removeInverterBySerial(inverter.Serial);
@@ -337,13 +342,18 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
// The order array contains list or id in the right order
JsonArray orderArray = root["order"].as();
uint8_t order = 0;
- for (JsonVariant id : orderArray) {
- uint8_t inverter_id = id.as();
- if (inverter_id < INV_MAX_COUNT) {
- INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id];
- inverter.Order = order;
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ for (JsonVariant id : orderArray) {
+ uint8_t inverter_id = id.as();
+ if (inverter_id < INV_MAX_COUNT) {
+ INVERTER_CONFIG_T& inverter = config.Inverter[inverter_id];
+ inverter.Order = order;
+ }
+ order++;
}
- order++;
}
WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");
diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp
index 0c8ee5c6e..7558110e3 100644
--- a/src/WebApi_mqtt.cpp
+++ b/src/WebApi_mqtt.cpp
@@ -271,36 +271,40 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
}
}
- CONFIG_T& config = Configuration.get();
- config.Mqtt.Enabled = root["mqtt_enabled"].as();
- config.Mqtt.Retain = root["mqtt_retain"].as();
- config.Mqtt.Tls.Enabled = root["mqtt_tls"].as();
- strlcpy(config.Mqtt.Tls.RootCaCert, root["mqtt_root_ca_cert"].as().c_str(), sizeof(config.Mqtt.Tls.RootCaCert));
- config.Mqtt.Tls.CertLogin = root["mqtt_tls_cert_login"].as();
- strlcpy(config.Mqtt.Tls.ClientCert, root["mqtt_client_cert"].as().c_str(), sizeof(config.Mqtt.Tls.ClientCert));
- strlcpy(config.Mqtt.Tls.ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt.Tls.ClientKey));
- config.Mqtt.Port = root["mqtt_port"].as();
- strlcpy(config.Mqtt.Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt.Hostname));
- strlcpy(config.Mqtt.ClientId, root["mqtt_clientid"].as().c_str(), sizeof(config.Mqtt.ClientId));
- strlcpy(config.Mqtt.Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt.Username));
- strlcpy(config.Mqtt.Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt.Password));
- strlcpy(config.Mqtt.Lwt.Topic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt.Lwt.Topic));
- strlcpy(config.Mqtt.Lwt.Value_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Online));
- strlcpy(config.Mqtt.Lwt.Value_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Offline));
- config.Mqtt.Lwt.Qos = root["mqtt_lwt_qos"].as();
- config.Mqtt.PublishInterval = root["mqtt_publish_interval"].as();
- config.Mqtt.CleanSession = root["mqtt_clean_session"].as();
- config.Mqtt.Hass.Enabled = root["mqtt_hass_enabled"].as();
- config.Mqtt.Hass.Expire = root["mqtt_hass_expire"].as();
- config.Mqtt.Hass.Retain = root["mqtt_hass_retain"].as();
- config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as();
- strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt.Hass.Topic));
-
- // Check if base topic was changed
- if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) {
- MqttHandleInverter.unsubscribeTopics();
- strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic));
- MqttHandleInverter.subscribeTopics();
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ config.Mqtt.Enabled = root["mqtt_enabled"].as();
+ config.Mqtt.Retain = root["mqtt_retain"].as();
+ config.Mqtt.Tls.Enabled = root["mqtt_tls"].as();
+ strlcpy(config.Mqtt.Tls.RootCaCert, root["mqtt_root_ca_cert"].as().c_str(), sizeof(config.Mqtt.Tls.RootCaCert));
+ config.Mqtt.Tls.CertLogin = root["mqtt_tls_cert_login"].as();
+ strlcpy(config.Mqtt.Tls.ClientCert, root["mqtt_client_cert"].as().c_str(), sizeof(config.Mqtt.Tls.ClientCert));
+ strlcpy(config.Mqtt.Tls.ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt.Tls.ClientKey));
+ config.Mqtt.Port = root["mqtt_port"].as();
+ strlcpy(config.Mqtt.Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt.Hostname));
+ strlcpy(config.Mqtt.ClientId, root["mqtt_clientid"].as().c_str(), sizeof(config.Mqtt.ClientId));
+ strlcpy(config.Mqtt.Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt.Username));
+ strlcpy(config.Mqtt.Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt.Password));
+ strlcpy(config.Mqtt.Lwt.Topic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt.Lwt.Topic));
+ strlcpy(config.Mqtt.Lwt.Value_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Online));
+ strlcpy(config.Mqtt.Lwt.Value_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Offline));
+ config.Mqtt.Lwt.Qos = root["mqtt_lwt_qos"].as();
+ config.Mqtt.PublishInterval = root["mqtt_publish_interval"].as();
+ config.Mqtt.CleanSession = root["mqtt_clean_session"].as();
+ config.Mqtt.Hass.Enabled = root["mqtt_hass_enabled"].as();
+ config.Mqtt.Hass.Expire = root["mqtt_hass_expire"].as();
+ config.Mqtt.Hass.Retain = root["mqtt_hass_retain"].as();
+ config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as();
+ strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt.Hass.Topic));
+
+ // Check if base topic was changed
+ if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) {
+ MqttHandleInverter.unsubscribeTopics();
+ strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic));
+ MqttHandleInverter.subscribeTopics();
+ }
}
WebApi.writeConfig(retMsg);
diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp
index 75275755f..51db32e43 100644
--- a/src/WebApi_network.cpp
+++ b/src/WebApi_network.cpp
@@ -164,37 +164,41 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
return;
}
- CONFIG_T& config = Configuration.get();
- config.WiFi.Ip[0] = ipaddress[0];
- config.WiFi.Ip[1] = ipaddress[1];
- config.WiFi.Ip[2] = ipaddress[2];
- config.WiFi.Ip[3] = ipaddress[3];
- config.WiFi.Netmask[0] = netmask[0];
- config.WiFi.Netmask[1] = netmask[1];
- config.WiFi.Netmask[2] = netmask[2];
- config.WiFi.Netmask[3] = netmask[3];
- config.WiFi.Gateway[0] = gateway[0];
- config.WiFi.Gateway[1] = gateway[1];
- config.WiFi.Gateway[2] = gateway[2];
- config.WiFi.Gateway[3] = gateway[3];
- config.WiFi.Dns1[0] = dns1[0];
- config.WiFi.Dns1[1] = dns1[1];
- config.WiFi.Dns1[2] = dns1[2];
- config.WiFi.Dns1[3] = dns1[3];
- config.WiFi.Dns2[0] = dns2[0];
- config.WiFi.Dns2[1] = dns2[1];
- config.WiFi.Dns2[2] = dns2[2];
- config.WiFi.Dns2[3] = dns2[3];
- strlcpy(config.WiFi.Ssid, root["ssid"].as().c_str(), sizeof(config.WiFi.Ssid));
- strlcpy(config.WiFi.Password, root["password"].as().c_str(), sizeof(config.WiFi.Password));
- strlcpy(config.WiFi.Hostname, root["hostname"].as().c_str(), sizeof(config.WiFi.Hostname));
- if (root["dhcp"].as()) {
- config.WiFi.Dhcp = true;
- } else {
- config.WiFi.Dhcp = false;
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ config.WiFi.Ip[0] = ipaddress[0];
+ config.WiFi.Ip[1] = ipaddress[1];
+ config.WiFi.Ip[2] = ipaddress[2];
+ config.WiFi.Ip[3] = ipaddress[3];
+ config.WiFi.Netmask[0] = netmask[0];
+ config.WiFi.Netmask[1] = netmask[1];
+ config.WiFi.Netmask[2] = netmask[2];
+ config.WiFi.Netmask[3] = netmask[3];
+ config.WiFi.Gateway[0] = gateway[0];
+ config.WiFi.Gateway[1] = gateway[1];
+ config.WiFi.Gateway[2] = gateway[2];
+ config.WiFi.Gateway[3] = gateway[3];
+ config.WiFi.Dns1[0] = dns1[0];
+ config.WiFi.Dns1[1] = dns1[1];
+ config.WiFi.Dns1[2] = dns1[2];
+ config.WiFi.Dns1[3] = dns1[3];
+ config.WiFi.Dns2[0] = dns2[0];
+ config.WiFi.Dns2[1] = dns2[1];
+ config.WiFi.Dns2[2] = dns2[2];
+ config.WiFi.Dns2[3] = dns2[3];
+ strlcpy(config.WiFi.Ssid, root["ssid"].as().c_str(), sizeof(config.WiFi.Ssid));
+ strlcpy(config.WiFi.Password, root["password"].as().c_str(), sizeof(config.WiFi.Password));
+ strlcpy(config.WiFi.Hostname, root["hostname"].as().c_str(), sizeof(config.WiFi.Hostname));
+ if (root["dhcp"].as()) {
+ config.WiFi.Dhcp = true;
+ } else {
+ config.WiFi.Dhcp = false;
+ }
+ config.WiFi.ApTimeout = root["aptimeout"].as();
+ config.Mdns.Enabled = root["mdnsenabled"].as();
}
- config.WiFi.ApTimeout = root["aptimeout"].as();
- config.Mdns.Enabled = root["mdnsenabled"].as();
WebApi.writeConfig(retMsg);
diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp
index 5dc874b53..f58a2bbd6 100644
--- a/src/WebApi_ntp.cpp
+++ b/src/WebApi_ntp.cpp
@@ -135,13 +135,17 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
return;
}
- CONFIG_T& config = Configuration.get();
- strlcpy(config.Ntp.Server, root["ntp_server"].as().c_str(), sizeof(config.Ntp.Server));
- strlcpy(config.Ntp.Timezone, root["ntp_timezone"].as().c_str(), sizeof(config.Ntp.Timezone));
- strlcpy(config.Ntp.TimezoneDescr, root["ntp_timezone_descr"].as().c_str(), sizeof(config.Ntp.TimezoneDescr));
- config.Ntp.Latitude = root["latitude"].as();
- config.Ntp.Longitude = root["longitude"].as();
- config.Ntp.SunsetType = root["sunsettype"].as();
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ strlcpy(config.Ntp.Server, root["ntp_server"].as().c_str(), sizeof(config.Ntp.Server));
+ strlcpy(config.Ntp.Timezone, root["ntp_timezone"].as().c_str(), sizeof(config.Ntp.Timezone));
+ strlcpy(config.Ntp.TimezoneDescr, root["ntp_timezone_descr"].as().c_str(), sizeof(config.Ntp.TimezoneDescr));
+ config.Ntp.Latitude = root["latitude"].as();
+ config.Ntp.Longitude = root["longitude"].as();
+ config.Ntp.SunsetType = root["sunsettype"].as();
+ }
WebApi.writeConfig(retMsg);
diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp
index 6be21ca6a..8ebd6fb6a 100644
--- a/src/WebApi_security.cpp
+++ b/src/WebApi_security.cpp
@@ -64,9 +64,13 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
return;
}
- CONFIG_T& config = Configuration.get();
- strlcpy(config.Security.Password, root["password"].as().c_str(), sizeof(config.Security.Password));
- config.Security.AllowReadonly = root["allow_readonly"].as();
+ {
+ auto guard = Configuration.getWriteGuard();
+ auto& config = guard.getConfig();
+
+ strlcpy(config.Security.Password, root["password"].as().c_str(), sizeof(config.Security.Password));
+ config.Security.AllowReadonly = root["allow_readonly"].as();
+ }
WebApi.writeConfig(retMsg);
diff --git a/src/main.cpp b/src/main.cpp
index b1d974d74..7b04e7cd6 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -65,11 +65,11 @@ void setup()
MessageOutput.println("done");
}
+ Configuration.init(scheduler);
+
// Read configuration values
MessageOutput.print("Reading configuration... ");
if (!Configuration.read()) {
- MessageOutput.print("initializing... ");
- Configuration.init();
if (Configuration.write()) {
MessageOutput.print("written... ");
} else {
@@ -146,19 +146,6 @@ void setup()
LedSingle.init(scheduler);
MessageOutput.println("done");
- // Check for default DTU serial
- MessageOutput.print("Check for default DTU serial... ");
- if (config.Dtu.Serial == DTU_SERIAL) {
- MessageOutput.print("generate serial based on ESP chip id: ");
- const uint64_t dtuId = Utils::generateDtuSerial();
- MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
- ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
- ((uint32_t)(dtuId & 0xFFFFFFFF)));
- config.Dtu.Serial = dtuId;
- Configuration.write();
- }
- MessageOutput.println("done");
-
InverterSettings.init(scheduler);
Datastore.init(scheduler);
From 17016b179f4838c1751a274475e601cd3aeca921 Mon Sep 17 00:00:00 2001
From: Bernhard Kirchen
Date: Tue, 22 Oct 2024 22:19:17 +0200
Subject: [PATCH 019/130] actions: use RELEASE_TOKEN to create a release
---
.github/workflows/build.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e955856a9..4d7b478ab 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -189,4 +189,4 @@ jobs:
files: |
artifacts/*.zip, artifacts/*.bin
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
From d36b30ae0e67f4f25117f86d707b7e3f2ccc983e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20Bo=CC=88hm?=
Date: Wed, 23 Oct 2024 21:47:50 +0200
Subject: [PATCH 020/130] Fix: SBS Unipower battery discharge current handling
SBS CAN receiver implementation was not using the correct way to provide
discharge current limit.
---
include/BatteryStats.h | 1 -
src/BatteryStats.cpp | 2 --
src/SBSCanReceiver.cpp | 6 +++---
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/include/BatteryStats.h b/include/BatteryStats.h
index bcf7cedc5..9f5e533d3 100644
--- a/include/BatteryStats.h
+++ b/include/BatteryStats.h
@@ -159,7 +159,6 @@ class SBSBatteryStats : public BatteryStats {
float _chargeVoltage;
float _chargeCurrentLimitation;
- float _dischargeCurrentLimitation;
uint16_t _stateOfHealth;
float _current;
float _temperature;
diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp
index 796158d2c..8f207ae2c 100644
--- a/src/BatteryStats.cpp
+++ b/src/BatteryStats.cpp
@@ -163,7 +163,6 @@ void SBSBatteryStats::getLiveViewData(JsonVariant& root) const
// values go into the "Status" card of the web application
addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1);
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1);
- addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
@@ -419,7 +418,6 @@ void SBSBatteryStats::mqttPublish() const
MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage));
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation));
- MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation));
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
MqttSettings.publish("battery/current", String(_current));
MqttSettings.publish("battery/temperature", String(_temperature));
diff --git a/src/SBSCanReceiver.cpp b/src/SBSCanReceiver.cpp
index 9150075c1..9d225d21c 100644
--- a/src/SBSCanReceiver.cpp
+++ b/src/SBSCanReceiver.cpp
@@ -74,10 +74,10 @@ void SBSCanReceiver::onMessage(twai_message_t rx_message)
case 0x640: {
_stats->_chargeCurrentLimitation = (this->readSignedInt24(rx_message.data + 3) * 0.001);
- _stats->_dischargeCurrentLimitation = (this->readSignedInt24(rx_message.data)) * 0.001;
+ _stats->setDischargeCurrentLimit(this->readSignedInt24(rx_message.data) * 0.001, millis());
if (_verboseLogging) {
- MessageOutput.printf("[SBS Unipower] 1600 Currents %f, %f \r\n", _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation);
+ MessageOutput.printf("[SBS Unipower] 1600 Currents %f, %f \r\n", _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit());
}
break;
}
@@ -144,7 +144,7 @@ void SBSCanReceiver::dummyData()
_stats->setSoC(42, 0/*precision*/, millis());
_stats->_chargeVoltage = dummyFloat(50);
_stats->_chargeCurrentLimitation = dummyFloat(33);
- _stats->_dischargeCurrentLimitation = dummyFloat(12);
+ _stats->setDischargeCurrentLimit(dummyFloat(12), millis());
_stats->_stateOfHealth = 99;
_stats->setVoltage(48.67, millis());
_stats->_current = dummyFloat(-1);
From 2e85b420d58acbaa366b28c6789762ec578d562c Mon Sep 17 00:00:00 2001
From: Tobias Diedrich
Date: Sat, 28 Sep 2024 16:40:34 +0200
Subject: [PATCH 021/130] Feature: add SoC & voltage thresholds for battery
current limit
This changes the custom current limit so the custom limit is only
applied when any of:
- SoC is valid and not ignored and SoC < threshold
- Voltage is valid and Voltage < threshold
- Voltage is invalid
Independently, if "Use Battery-Reported limit" is enabled and valid, it
is applied (unless a lower custom limit already was applied).
---
include/Configuration.h | 2 ++
include/defaults.h | 2 ++
src/Battery.cpp | 20 ++++++++++++++++----
src/Configuration.cpp | 4 ++++
webapp/src/locales/de.json | 4 ++++
webapp/src/locales/en.json | 4 ++++
webapp/src/types/BatteryConfig.ts | 2 ++
webapp/src/views/BatteryAdminView.vue | 22 ++++++++++++++++++++++
8 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/include/Configuration.h b/include/Configuration.h
index 3b99c38bb..49b1c60cc 100644
--- a/include/Configuration.h
+++ b/include/Configuration.h
@@ -145,6 +145,8 @@ struct BATTERY_CONFIG_T {
BatteryVoltageUnit MqttVoltageUnit;
bool EnableDischargeCurrentLimit;
float DischargeCurrentLimit;
+ float DischargeCurrentLimitBelowSoc;
+ float DischargeCurrentLimitBelowVoltage;
bool UseBatteryReportedDischargeCurrentLimit;
char MqttDischargeCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttDischargeCurrentJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1];
diff --git a/include/defaults.h b/include/defaults.h
index 2ab8ec9e8..40ee5af12 100644
--- a/include/defaults.h
+++ b/include/defaults.h
@@ -156,6 +156,8 @@
#define BATTERY_JKBMS_POLLING_INTERVAL 5
#define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false
#define BATTERY_DISCHARGE_CURRENT_LIMIT 0.0
+#define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC 100.0
+#define BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_VOLTAGE 60.0
#define BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT false
#define HUAWEI_ENABLED false
diff --git a/src/Battery.cpp b/src/Battery.cpp
index 7483c1de4..106888e2b 100644
--- a/src/Battery.cpp
+++ b/src/Battery.cpp
@@ -91,14 +91,26 @@ float BatteryClass::getDischargeCurrentLimit()
if (!config.Battery.EnableDischargeCurrentLimit) { return FLT_MAX; }
auto dischargeCurrentLimit = config.Battery.DischargeCurrentLimit;
- auto dischargeCurrentValid = dischargeCurrentLimit > 0.0f;
-
+ auto dischargeCurrentLimitValid = dischargeCurrentLimit > 0.0f;
+ auto dischargeCurrentLimitBelowSoc = config.Battery.DischargeCurrentLimitBelowSoc;
+ auto dischargeCurrentLimitBelowVoltage = config.Battery.DischargeCurrentLimitBelowVoltage;
+ auto statsSoCValid = getStats()->getSoCAgeSeconds() <= 60 && !config.PowerLimiter.IgnoreSoc;
+ auto statsSoC = statsSoCValid ? getStats()->getSoC() : 100.0; // fail open so we use voltage instead
+ auto statsVoltageValid = getStats()->getVoltageAgeSeconds() <= 60;
+ auto statsVoltage = statsVoltageValid ? getStats()->getVoltage() : 0.0; // fail closed
auto statsCurrentLimit = getStats()->getDischargeCurrentLimit();
auto statsLimitValid = config.Battery.UseBatteryReportedDischargeCurrentLimit
&& statsCurrentLimit >= 0.0f
&& getStats()->getDischargeCurrentLimitAgeSeconds() <= 60;
- if (statsLimitValid && dischargeCurrentValid) {
+
+ if (statsSoC > dischargeCurrentLimitBelowSoc && statsVoltage > dischargeCurrentLimitBelowVoltage) {
+ // Above SoC and Voltage thresholds, ignore custom limit.
+ // Battery-provided limit will still be applied.
+ dischargeCurrentLimitValid = false;
+ }
+
+ if (statsLimitValid && dischargeCurrentLimitValid) {
// take the lowest limit
return min(statsCurrentLimit, dischargeCurrentLimit);
}
@@ -107,7 +119,7 @@ float BatteryClass::getDischargeCurrentLimit()
return statsCurrentLimit;
}
- if (dischargeCurrentValid) {
+ if (dischargeCurrentLimitValid) {
return dischargeCurrentLimit;
}
diff --git a/src/Configuration.cpp b/src/Configuration.cpp
index 61ee7758d..b260ccee2 100644
--- a/src/Configuration.cpp
+++ b/src/Configuration.cpp
@@ -88,6 +88,8 @@ void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, Jso
target["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit;
target["enable_discharge_current_limit"] = config.Battery.EnableDischargeCurrentLimit;
target["discharge_current_limit"] = config.Battery.DischargeCurrentLimit;
+ target["discharge_current_limit_below_soc"] = config.Battery.DischargeCurrentLimitBelowSoc;
+ target["discharge_current_limit_below_voltage"] = config.Battery.DischargeCurrentLimitBelowVoltage;
target["use_battery_reported_discharge_current_limit"] = config.Battery.UseBatteryReportedDischargeCurrentLimit;
target["mqtt_discharge_current_topic"] = config.Battery.MqttDischargeCurrentTopic;
target["mqtt_discharge_current_json_path"] = config.Battery.MqttDischargeCurrentJsonPath;
@@ -383,6 +385,8 @@ void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, Batt
target.MqttVoltageUnit = source["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts;
target.EnableDischargeCurrentLimit = source["enable_discharge_current_limit"] | BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT;
target.DischargeCurrentLimit = source["discharge_current_limit"] | BATTERY_DISCHARGE_CURRENT_LIMIT;
+ target.DischargeCurrentLimitBelowSoc = source["discharge_current_limit_below_soc"] | BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_SOC;
+ target.DischargeCurrentLimitBelowVoltage = source["discharge_current_limit_below_voltage"] | BATTERY_DISCHARGE_CURRENT_LIMIT_BELOW_VOLTAGE;
target.UseBatteryReportedDischargeCurrentLimit = source["use_battery_reported_discharge_current_limit"] | BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT;
strlcpy(target.MqttDischargeCurrentTopic, source["mqtt_discharge_current_topic"] | "", sizeof(config.Battery.MqttDischargeCurrentTopic));
strlcpy(target.MqttDischargeCurrentJsonPath, source["mqtt_discharge_current_json_path"] | "", sizeof(config.Battery.MqttDischargeCurrentJsonPath));
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json
index 1a16673b3..b2d7de185 100644
--- a/webapp/src/locales/de.json
+++ b/webapp/src/locales/de.json
@@ -724,6 +724,10 @@
"DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit",
"LimitDischargeCurrent": "Entladestrom limitieren",
"DischargeCurrentLimit": "max. Entladestrom",
+ "DischargeCurrentLimitBelowSoc": "Limitieren unter SoC",
+ "DischargeCurrentLimitBelowSocInfo": "Das Entladestromlimit wird nur unter dieser SoC-Schwelle angewendet (nicht verwendet falls 'Batterie SoC ignorieren' aktiviert ist).",
+ "DischargeCurrentLimitBelowVoltage": "Limitieren unter Spannung",
+ "DischargeCurrentLimitBelowVoltageInfo": "Das Entladestromlimit wird nur unter dieser Spannungs-Schwelle angewendet (wenn SoC ignoriert oder nicht verfügbar).",
"UseBatteryReportedDischargeCurrentLimit": "Von der Batterie übermitteltes Limit verwenden",
"BatteryReportedDischargeCurrentLimitInfo": "Hinweis: Das niedrigste Limit wird angewendet, wobei das von der Batterie übermittelte Entladestromlimit nur verwendet wird, wenn in der letzten Minute ein Update eingegangen ist; andernfalls dient das zuvor festgelegte Limit als Fallback.",
"MqttDischargeCurrentTopic": "Topic für Entladestromlimit",
diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json
index 69a288778..425ad9972 100644
--- a/webapp/src/locales/en.json
+++ b/webapp/src/locales/en.json
@@ -727,6 +727,10 @@
"DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings",
"LimitDischargeCurrent": "Limit Discharge Current",
"DischargeCurrentLimit": "max. Discharge Current",
+ "DischargeCurrentLimitBelowSoc": "Apply limit below SoC",
+ "DischargeCurrentLimitBelowSocInfo": "The discharge current limit is not applied above this SoC (not used if 'Ignore Battery SoC' is enabled).",
+ "DischargeCurrentLimitBelowVoltage": "Apply limit below voltage",
+ "DischargeCurrentLimitBelowVoltageInfo": "The discharge current limit is not applied above this voltage (used if SoC ignored or unavailable).",
"UseBatteryReportedDischargeCurrentLimit": "Use Battery-Reported limit",
"BatteryReportedDischargeCurrentLimitInfo": "Hint: The lowest limit will be applied, with the battery-reported discharge current limit used only if an update was received in the last minute; otherwise, the previously specified limit will act as a fallback.",
"MqttDischargeCurrentTopic": "Discharge Current Limit Value Topic",
diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts
index a781b74a0..67792a710 100644
--- a/webapp/src/types/BatteryConfig.ts
+++ b/webapp/src/types/BatteryConfig.ts
@@ -11,6 +11,8 @@ export interface BatteryConfig {
mqtt_voltage_unit: number;
enable_discharge_current_limit: boolean;
discharge_current_limit: number;
+ discharge_current_limit_below_soc: number;
+ discharge_current_limit_below_voltage: number;
use_battery_reported_discharge_current_limit: boolean;
mqtt_discharge_current_topic: string;
mqtt_discharge_current_json_path: string;
diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue
index 4a07357c7..b55b88e51 100644
--- a/webapp/src/views/BatteryAdminView.vue
+++ b/webapp/src/views/BatteryAdminView.vue
@@ -141,6 +141,28 @@
postfix="A"
/>
+
+
+
+
Date: Wed, 23 Oct 2024 21:00:52 +0200
Subject: [PATCH 022/130] webapp: prettify battery settings UI
* use wide labels for all battery settings
* dynamically show and hide valid battery discharge limit settings
---
webapp/src/locales/de.json | 4 +-
webapp/src/locales/en.json | 4 +-
webapp/src/views/BatteryAdminView.vue | 128 +++++++++++++++-----------
3 files changed, 79 insertions(+), 57 deletions(-)
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json
index b2d7de185..6bf5f9776 100644
--- a/webapp/src/locales/de.json
+++ b/webapp/src/locales/de.json
@@ -725,9 +725,9 @@
"LimitDischargeCurrent": "Entladestrom limitieren",
"DischargeCurrentLimit": "max. Entladestrom",
"DischargeCurrentLimitBelowSoc": "Limitieren unter SoC",
- "DischargeCurrentLimitBelowSocInfo": "Das Entladestromlimit wird nur unter dieser SoC-Schwelle angewendet (nicht verwendet falls 'Batterie SoC ignorieren' aktiviert ist).",
+ "DischargeCurrentLimitBelowSocInfo": "Das Entladestromlimit wird nur unter dieser SoC-Schwelle angewendet (wird nicht verwendet, falls 'Batterie SoC ignorieren' in den DPL-Einstellungen aktiviert ist).",
"DischargeCurrentLimitBelowVoltage": "Limitieren unter Spannung",
- "DischargeCurrentLimitBelowVoltageInfo": "Das Entladestromlimit wird nur unter dieser Spannungs-Schwelle angewendet (wenn SoC ignoriert oder nicht verfügbar).",
+ "DischargeCurrentLimitBelowVoltageInfo": "Das Entladestromlimit wird nur unter dieser Spannungs-Schwelle angewendet (wenn 'Batterie SoC ignorieren' in den DPL-Einstellungen aktiviert ist oder SoC nicht verfügbar ist).",
"UseBatteryReportedDischargeCurrentLimit": "Von der Batterie übermitteltes Limit verwenden",
"BatteryReportedDischargeCurrentLimitInfo": "Hinweis: Das niedrigste Limit wird angewendet, wobei das von der Batterie übermittelte Entladestromlimit nur verwendet wird, wenn in der letzten Minute ein Update eingegangen ist; andernfalls dient das zuvor festgelegte Limit als Fallback.",
"MqttDischargeCurrentTopic": "Topic für Entladestromlimit",
diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json
index 425ad9972..2ade82335 100644
--- a/webapp/src/locales/en.json
+++ b/webapp/src/locales/en.json
@@ -728,9 +728,9 @@
"LimitDischargeCurrent": "Limit Discharge Current",
"DischargeCurrentLimit": "max. Discharge Current",
"DischargeCurrentLimitBelowSoc": "Apply limit below SoC",
- "DischargeCurrentLimitBelowSocInfo": "The discharge current limit is not applied above this SoC (not used if 'Ignore Battery SoC' is enabled).",
+ "DischargeCurrentLimitBelowSocInfo": "The discharge current limit is only applied below this SoC (not used if 'Ignore Battery SoC' is enabled in the DPL settings).",
"DischargeCurrentLimitBelowVoltage": "Apply limit below voltage",
- "DischargeCurrentLimitBelowVoltageInfo": "The discharge current limit is not applied above this voltage (used if SoC ignored or unavailable).",
+ "DischargeCurrentLimitBelowVoltageInfo": "The discharge current limit is only applied below this voltage (used if 'Ignore Battery SoC' is enabled in the DPL settings or when SoC is unavailable).",
"UseBatteryReportedDischargeCurrentLimit": "Use Battery-Reported limit",
"BatteryReportedDischargeCurrentLimitInfo": "Hint: The lowest limit will be applied, with the battery-reported discharge current limit used only if an update was received in the last minute; otherwise, the previously specified limit will act as a fallback.",
"MqttDischargeCurrentTopic": "Discharge Current Limit Value Topic",
diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue
index b55b88e51..46bd5da79 100644
--- a/webapp/src/views/BatteryAdminView.vue
+++ b/webapp/src/views/BatteryAdminView.vue
@@ -10,6 +10,7 @@
:label="$t('batteryadmin.EnableBattery')"
v-model="batteryConfigList.enabled"
type="checkbox"
+ wide
/>
-