Merge branch 'development'

This commit is contained in:
helgeerbe 2024-01-07 19:09:26 +01:00
commit ffd189c1f5
68 changed files with 805 additions and 649 deletions

View File

@ -5,7 +5,7 @@
#include <cstdint> #include <cstdint>
#define CONFIG_FILENAME "/config.json" #define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011900 // 0.1.24 // make sure to clean all after change #define CONFIG_VERSION 0x00011a00 // 0.1.26 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_PASSWORD_STRLEN 64

View File

@ -2,6 +2,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <frozen/string.h>
#include "Battery.h" #include "Battery.h"
#include "JkBmsSerialMessage.h" #include "JkBmsSerialMessage.h"
@ -30,7 +31,7 @@ class Controller : public BatteryProvider {
FrameCompleted FrameCompleted
}; };
std::string const& getStatusText(Status status); frozen::string const& getStatusText(Status status);
void announceStatus(Status status); void announceStatus(Status status);
void sendRequest(uint8_t pollInterval); void sendRequest(uint8_t pollInterval);
void rxData(uint8_t inbyte); void rxData(uint8_t inbyte);

View File

@ -6,6 +6,8 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <variant> #include <variant>
#include <frozen/map.h>
#include <frozen/string.h>
namespace JkBms { namespace JkBms {
@ -33,7 +35,7 @@ enum class AlarmBits : uint16_t {
#undef ALARM_ENUM #undef ALARM_ENUM
}; };
static const std::map<AlarmBits, std::string> AlarmBitTexts = { static const frozen::map<AlarmBits, frozen::string, 16> AlarmBitTexts = {
#define ALARM_TEXT(name, value) { AlarmBits::name, #name }, #define ALARM_TEXT(name, value) { AlarmBits::name, #name },
ALARM_BITS(ALARM_TEXT) ALARM_BITS(ALARM_TEXT)
#undef ALARM_TEXT #undef ALARM_TEXT
@ -51,7 +53,7 @@ enum class StatusBits : uint16_t {
#undef STATUS_ENUM #undef STATUS_ENUM
}; };
static const std::map<StatusBits, std::string> StatusBitTexts = { static const frozen::map<StatusBits, frozen::string, 4> StatusBitTexts = {
#define STATUS_TEXT(name, value) { StatusBits::name, #name }, #define STATUS_TEXT(name, value) { StatusBits::name, #name },
STATUS_BITS(STATUS_TEXT) STATUS_BITS(STATUS_TEXT)
#undef STATUS_TEXT #undef STATUS_TEXT

View File

@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include <functional> #include <functional>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <frozen/string.h>
#define PL_UI_STATE_INACTIVE 0 #define PL_UI_STATE_INACTIVE 0
#define PL_UI_STATE_CHARGING 1 #define PL_UI_STATE_CHARGING 1
@ -83,7 +84,7 @@ private:
bool _fullSolarPassThroughEnabled = false; bool _fullSolarPassThroughEnabled = false;
bool _verboseLogging = true; bool _verboseLogging = true;
std::string const& getStatusText(Status status); frozen::string const& getStatusText(Status status);
void announceStatus(Status status); void announceStatus(Status status);
bool shutdown(Status status); bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); } bool shutdown() { return shutdown(_lastStatus); }

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <ArduinoJson.h>
#include <cstdint> #include <cstdint>
class Utils { class Utils {
@ -9,4 +10,5 @@ public:
static uint64_t generateDtuSerial(); static uint64_t generateDtuSerial();
static int getTimezoneOffset(); static int getTimezoneOffset();
static void restartDtu(); static void restartDtu();
static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
}; };

View File

@ -11,6 +11,7 @@ public:
std::shared_ptr<BatteryStats> getStats() const final { return _stats; } std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
private: private:
uint32_t _lastUpdate = 0;
std::shared_ptr<VictronSmartShuntStats> _stats = std::shared_ptr<VictronSmartShuntStats> _stats =
std::make_shared<VictronSmartShuntStats>(); std::make_shared<VictronSmartShuntStats>();
}; };

View File

@ -6,6 +6,7 @@
#include "WebApi_device.h" #include "WebApi_device.h"
#include "WebApi_devinfo.h" #include "WebApi_devinfo.h"
#include "WebApi_dtu.h" #include "WebApi_dtu.h"
#include "WebApi_errors.h"
#include "WebApi_eventlog.h" #include "WebApi_eventlog.h"
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "WebApi_gridprofile.h" #include "WebApi_gridprofile.h"
@ -42,6 +43,8 @@ public:
static void sendTooManyRequests(AsyncWebServerRequest* request); static void sendTooManyRequests(AsyncWebServerRequest* request);
static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!");
private: private:
void loop(); void loop();

View File

@ -8,7 +8,7 @@ class WebApiHuaweiClass {
public: public:
void init(AsyncWebServer& server); void init(AsyncWebServer& server);
void loop(); void loop();
void getJsonData(JsonObject& root); void getJsonData(JsonVariant& root);
private: private:
void onStatus(AsyncWebServerRequest* request); void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request);

View File

@ -8,6 +8,7 @@ enum WebApiError {
GenericDataTooLarge, GenericDataTooLarge,
GenericParseError, GenericParseError,
GenericValueMissing, GenericValueMissing,
GenericWriteFailed,
DtuBase = 2000, DtuBase = 2000,
DtuSerialZero, DtuSerialZero,

View File

@ -3,7 +3,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
//#include <HuaweiFrameHandler.h> #include <mutex>
class WebApiWsHuaweiLiveClass { class WebApiWsHuaweiLiveClass {
public: public:
@ -21,4 +21,6 @@ private:
uint32_t _lastWsCleanup = 0; uint32_t _lastWsCleanup = 0;
uint32_t _lastUpdateCheck = 0; uint32_t _lastUpdateCheck = 0;
std::mutex _mutex;
}; };

View File

@ -3,6 +3,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <mutex>
class WebApiWsBatteryLiveClass { class WebApiWsBatteryLiveClass {
public: public:
@ -21,4 +22,6 @@ private:
uint32_t _lastWsCleanup = 0; uint32_t _lastWsCleanup = 0;
uint32_t _lastUpdateCheck = 0; uint32_t _lastUpdateCheck = 0;
static constexpr uint16_t _responseSize = 1024 + 512; static constexpr uint16_t _responseSize = 1024 + 512;
std::mutex _mutex;
}; };

View File

@ -4,6 +4,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <VeDirectMpptController.h> #include <VeDirectMpptController.h>
#include <mutex>
class WebApiWsVedirectLiveClass { class WebApiWsVedirectLiveClass {
public: public:
@ -23,4 +24,6 @@ private:
uint32_t _lastWsCleanup = 0; uint32_t _lastWsCleanup = 0;
uint32_t _dataAgeMillis = 0; uint32_t _dataAgeMillis = 0;
static constexpr uint16_t _responseSize = 1024 + 128; static constexpr uint16_t _responseSize = 1024 + 128;
std::mutex _mutex;
}; };

View File

@ -13,7 +13,7 @@
#define AUTH_USERNAME "admin" #define AUTH_USERNAME "admin"
#define SECURITY_ALLOW_READONLY true #define SECURITY_ALLOW_READONLY true
#define WIFI_RECONNECT_TIMEOUT 15 #define WIFI_RECONNECT_TIMEOUT 30
#define WIFI_RECONNECT_REDO_TIMEOUT 600 #define WIFI_RECONNECT_REDO_TIMEOUT 600
#define WIFI_SSID "" #define WIFI_SSID ""

View File

@ -295,106 +295,90 @@ uint32_t VeDirectFrameHandler::getLastUpdate() const
return _lastUpdate; return _lastUpdate;
} }
template<typename T>
String const& VeDirectFrameHandler::getAsString(std::map<T, String> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static String dummy;
dummy = val;
return dummy;
}
return pos->second;
}
template String const& VeDirectFrameHandler::getAsString(std::map<uint8_t, String> const& values, uint8_t val);
template String const& VeDirectFrameHandler::getAsString(std::map<uint16_t, String> const& values, uint16_t val);
template String const& VeDirectFrameHandler::getAsString(std::map<uint32_t, String> const& values, uint32_t val);
/* /*
* getPidAsString * getPidAsString
* This function returns the product id (PID) as readable text. * This function returns the product id (PID) as readable text.
*/ */
String VeDirectFrameHandler::veStruct::getPidAsString() const frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const
{ {
static const std::map<uint16_t, String> values = { static constexpr frozen::map<uint16_t, frozen::string, 77> values = {
{ 0x0300, F("BlueSolar MPPT 70|15") }, { 0x0300, "BlueSolar MPPT 70|15" },
{ 0xA040, F("BlueSolar MPPT 75|50") }, { 0xA040, "BlueSolar MPPT 75|50" },
{ 0xA041, F("BlueSolar MPPT 150|35") }, { 0xA041, "BlueSolar MPPT 150|35" },
{ 0xA042, F("BlueSolar MPPT 75|15") }, { 0xA042, "BlueSolar MPPT 75|15" },
{ 0xA043, F("BlueSolar MPPT 100|15") }, { 0xA043, "BlueSolar MPPT 100|15" },
{ 0xA044, F("BlueSolar MPPT 100|30") }, { 0xA044, "BlueSolar MPPT 100|30" },
{ 0xA045, F("BlueSolar MPPT 100|50") }, { 0xA045, "BlueSolar MPPT 100|50" },
{ 0xA046, F("BlueSolar MPPT 100|70") }, { 0xA046, "BlueSolar MPPT 100|70" },
{ 0xA047, F("BlueSolar MPPT 150|100") }, { 0xA047, "BlueSolar MPPT 150|100" },
{ 0xA049, F("BlueSolar MPPT 100|50 rev2") }, { 0xA049, "BlueSolar MPPT 100|50 rev2" },
{ 0xA04A, F("BlueSolar MPPT 100|30 rev2") }, { 0xA04A, "BlueSolar MPPT 100|30 rev2" },
{ 0xA04B, F("BlueSolar MPPT 150|35 rev2") }, { 0xA04B, "BlueSolar MPPT 150|35 rev2" },
{ 0xA04C, F("BlueSolar MPPT 75|10") }, { 0xA04C, "BlueSolar MPPT 75|10" },
{ 0xA04D, F("BlueSolar MPPT 150|45") }, { 0xA04D, "BlueSolar MPPT 150|45" },
{ 0xA04E, F("BlueSolar MPPT 150|60") }, { 0xA04E, "BlueSolar MPPT 150|60" },
{ 0xA04F, F("BlueSolar MPPT 150|85") }, { 0xA04F, "BlueSolar MPPT 150|85" },
{ 0xA050, F("SmartSolar MPPT 250|100") }, { 0xA050, "SmartSolar MPPT 250|100" },
{ 0xA051, F("SmartSolar MPPT 150|100") }, { 0xA051, "SmartSolar MPPT 150|100" },
{ 0xA052, F("SmartSolar MPPT 150|85") }, { 0xA052, "SmartSolar MPPT 150|85" },
{ 0xA053, F("SmartSolar MPPT 75|15") }, { 0xA053, "SmartSolar MPPT 75|15" },
{ 0xA054, F("SmartSolar MPPT 75|10") }, { 0xA054, "SmartSolar MPPT 75|10" },
{ 0xA055, F("SmartSolar MPPT 100|15") }, { 0xA055, "SmartSolar MPPT 100|15" },
{ 0xA056, F("SmartSolar MPPT 100|30") }, { 0xA056, "SmartSolar MPPT 100|30" },
{ 0xA057, F("SmartSolar MPPT 100|50") }, { 0xA057, "SmartSolar MPPT 100|50" },
{ 0xA058, F("SmartSolar MPPT 150|35") }, { 0xA058, "SmartSolar MPPT 150|35" },
{ 0xA059, F("SmartSolar MPPT 150|10 rev2") }, { 0xA059, "SmartSolar MPPT 150|10 rev2" },
{ 0xA05A, F("SmartSolar MPPT 150|85 rev2") }, { 0xA05A, "SmartSolar MPPT 150|85 rev2" },
{ 0xA05B, F("SmartSolar MPPT 250|70") }, { 0xA05B, "SmartSolar MPPT 250|70" },
{ 0xA05C, F("SmartSolar MPPT 250|85") }, { 0xA05C, "SmartSolar MPPT 250|85" },
{ 0xA05D, F("SmartSolar MPPT 250|60") }, { 0xA05D, "SmartSolar MPPT 250|60" },
{ 0xA05E, F("SmartSolar MPPT 250|45") }, { 0xA05E, "SmartSolar MPPT 250|45" },
{ 0xA05F, F("SmartSolar MPPT 100|20") }, { 0xA05F, "SmartSolar MPPT 100|20" },
{ 0xA060, F("SmartSolar MPPT 100|20 48V") }, { 0xA060, "SmartSolar MPPT 100|20 48V" },
{ 0xA061, F("SmartSolar MPPT 150|45") }, { 0xA061, "SmartSolar MPPT 150|45" },
{ 0xA062, F("SmartSolar MPPT 150|60") }, { 0xA062, "SmartSolar MPPT 150|60" },
{ 0xA063, F("SmartSolar MPPT 150|70") }, { 0xA063, "SmartSolar MPPT 150|70" },
{ 0xA064, F("SmartSolar MPPT 250|85 rev2") }, { 0xA064, "SmartSolar MPPT 250|85 rev2" },
{ 0xA065, F("SmartSolar MPPT 250|100 rev2") }, { 0xA065, "SmartSolar MPPT 250|100 rev2" },
{ 0xA066, F("BlueSolar MPPT 100|20") }, { 0xA066, "BlueSolar MPPT 100|20" },
{ 0xA067, F("BlueSolar MPPT 100|20 48V") }, { 0xA067, "BlueSolar MPPT 100|20 48V" },
{ 0xA068, F("SmartSolar MPPT 250|60 rev2") }, { 0xA068, "SmartSolar MPPT 250|60 rev2" },
{ 0xA069, F("SmartSolar MPPT 250|70 rev2") }, { 0xA069, "SmartSolar MPPT 250|70 rev2" },
{ 0xA06A, F("SmartSolar MPPT 150|45 rev2") }, { 0xA06A, "SmartSolar MPPT 150|45 rev2" },
{ 0xA06B, F("SmartSolar MPPT 150|60 rev2") }, { 0xA06B, "SmartSolar MPPT 150|60 rev2" },
{ 0xA06C, F("SmartSolar MPPT 150|70 rev2") }, { 0xA06C, "SmartSolar MPPT 150|70 rev2" },
{ 0xA06D, F("SmartSolar MPPT 150|85 rev3") }, { 0xA06D, "SmartSolar MPPT 150|85 rev3" },
{ 0xA06E, F("SmartSolar MPPT 150|100 rev3") }, { 0xA06E, "SmartSolar MPPT 150|100 rev3" },
{ 0xA06F, F("BlueSolar MPPT 150|45 rev2") }, { 0xA06F, "BlueSolar MPPT 150|45 rev2" },
{ 0xA070, F("BlueSolar MPPT 150|60 rev2") }, { 0xA070, "BlueSolar MPPT 150|60 rev2" },
{ 0xA071, F("BlueSolar MPPT 150|70 rev2") }, { 0xA071, "BlueSolar MPPT 150|70 rev2" },
{ 0xA102, F("SmartSolar MPPT VE.Can 150|70") }, { 0xA102, "SmartSolar MPPT VE.Can 150|70" },
{ 0xA103, F("SmartSolar MPPT VE.Can 150|45") }, { 0xA103, "SmartSolar MPPT VE.Can 150|45" },
{ 0xA104, F("SmartSolar MPPT VE.Can 150|60") }, { 0xA104, "SmartSolar MPPT VE.Can 150|60" },
{ 0xA105, F("SmartSolar MPPT VE.Can 150|85") }, { 0xA105, "SmartSolar MPPT VE.Can 150|85" },
{ 0xA106, F("SmartSolar MPPT VE.Can 150|100") }, { 0xA106, "SmartSolar MPPT VE.Can 150|100" },
{ 0xA107, F("SmartSolar MPPT VE.Can 250|45") }, { 0xA107, "SmartSolar MPPT VE.Can 250|45" },
{ 0xA108, F("SmartSolar MPPT VE.Can 250|60") }, { 0xA108, "SmartSolar MPPT VE.Can 250|60" },
{ 0xA109, F("SmartSolar MPPT VE.Can 250|80") }, { 0xA109, "SmartSolar MPPT VE.Can 250|80" },
{ 0xA10A, F("SmartSolar MPPT VE.Can 250|85") }, { 0xA10A, "SmartSolar MPPT VE.Can 250|85" },
{ 0xA10B, F("SmartSolar MPPT VE.Can 250|100") }, { 0xA10B, "SmartSolar MPPT VE.Can 250|100" },
{ 0xA10C, F("SmartSolar MPPT VE.Can 150|70 rev2") }, { 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" },
{ 0xA10D, F("SmartSolar MPPT VE.Can 150|85 rev2") }, { 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" },
{ 0xA10E, F("SmartSolar MPPT VE.Can 150|100 rev2") }, { 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" },
{ 0xA10F, F("BlueSolar MPPT VE.Can 150|100") }, { 0xA10F, "BlueSolar MPPT VE.Can 150|100" },
{ 0xA110, F("SmartSolar MPPT RS 450|100") }, { 0xA110, "SmartSolar MPPT RS 450|100" },
{ 0xA112, F("BlueSolar MPPT VE.Can 250|70") }, { 0xA112, "BlueSolar MPPT VE.Can 250|70" },
{ 0xA113, F("BlueSolar MPPT VE.Can 250|100") }, { 0xA113, "BlueSolar MPPT VE.Can 250|100" },
{ 0xA114, F("SmartSolar MPPT VE.Can 250|70 rev2") }, { 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" },
{ 0xA115, F("SmartSolar MPPT VE.Can 250|100 rev2") }, { 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" },
{ 0xA116, F("SmartSolar MPPT VE.Can 250|85 rev2") }, { 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" },
{ 0xA381, F("BMV-712 Smart") }, { 0xA381, "BMV-712 Smart" },
{ 0xA382, F("BMV-710H Smart") }, { 0xA382, "BMV-710H Smart" },
{ 0xA383, F("BMV-712 Smart Rev2") }, { 0xA383, "BMV-712 Smart Rev2" },
{ 0xA389, F("SmartShunt 500A/50mV") }, { 0xA389, "SmartShunt 500A/50mV" },
{ 0xA38A, F("SmartShunt 1000A/50mV") }, { 0xA38A, "SmartShunt 1000A/50mV" },
{ 0xA38B, F("SmartShunt 2000A/50mV") }, { 0xA38B, "SmartShunt 2000A/50mV" },
{ 0xA3F0, F("SmartShunt 2000A/50mV" ) } { 0xA3F0, "SmartShunt 2000A/50mV" }
}; };
return getAsString(values, PID); return getAsString(values, PID);

View File

@ -13,7 +13,8 @@
#include <Arduino.h> #include <Arduino.h>
#include <array> #include <array>
#include <map> #include <frozen/string.h>
#include <frozen/map.h>
#include <memory> #include <memory>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 #define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
@ -39,14 +40,22 @@ protected:
double I = 0; // battery current in A double I = 0; // battery current in A
double E = 0; // efficiency in percent (calculated, moving average) double E = 0; // efficiency in percent (calculated, moving average)
String getPidAsString() const; // product id as string frozen::string const& getPidAsString() const; // product ID as string
} veStruct; } veStruct;
bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame); bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame);
bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated
template<typename T> template<typename T, size_t L>
static String const& getAsString(std::map<T, String> const& values, T val); static frozen::string const& getAsString(frozen::map<T, frozen::string, L> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static constexpr frozen::string dummy("???");
return dummy;
}
return pos->second;
}
private: private:
void setLastUpdate(); // set timestampt after successful frame read void setLastUpdate(); // set timestampt after successful frame read

View File

@ -1,5 +1,4 @@
#include <Arduino.h> #include <Arduino.h>
#include <map>
#include "VeDirectMpptController.h" #include "VeDirectMpptController.h"
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
@ -90,18 +89,18 @@ void VeDirectMpptController::frameValidEvent() {
* getCsAsString * getCsAsString
* This function returns the state of operations (CS) as readable text. * This function returns the state of operations (CS) as readable text.
*/ */
String VeDirectMpptController::veMpptStruct::getCsAsString() const frozen::string const& VeDirectMpptController::veMpptStruct::getCsAsString() const
{ {
static const std::map<uint8_t, String> values = { static constexpr frozen::map<uint8_t, frozen::string, 9> values = {
{ 0, F("OFF") }, { 0, "OFF" },
{ 2, F("Fault") }, { 2, "Fault" },
{ 3, F("Bulk") }, { 3, "Bulk" },
{ 4, F("Absorbtion") }, { 4, "Absorbtion" },
{ 5, F("Float") }, { 5, "Float" },
{ 7, F("Equalize (manual)") }, { 7, "Equalize (manual)" },
{ 245, F("Starting-up") }, { 245, "Starting-up" },
{ 247, F("Auto equalize / Recondition") }, { 247, "Auto equalize / Recondition" },
{ 252, F("External Control") } { 252, "External Control" }
}; };
return getAsString(values, CS); return getAsString(values, CS);
@ -111,12 +110,12 @@ String VeDirectMpptController::veMpptStruct::getCsAsString() const
* getMpptAsString * getMpptAsString
* This function returns the state of MPPT (MPPT) as readable text. * This function returns the state of MPPT (MPPT) as readable text.
*/ */
String VeDirectMpptController::veMpptStruct::getMpptAsString() const frozen::string const& VeDirectMpptController::veMpptStruct::getMpptAsString() const
{ {
static const std::map<uint8_t, String> values = { static constexpr frozen::map<uint8_t, frozen::string, 3> values = {
{ 0, F("OFF") }, { 0, "OFF" },
{ 1, F("Voltage or current limited") }, { 1, "Voltage or current limited" },
{ 2, F("MPP Tracker active") } { 2, "MPP Tracker active" }
}; };
return getAsString(values, MPPT); return getAsString(values, MPPT);
@ -126,29 +125,29 @@ String VeDirectMpptController::veMpptStruct::getMpptAsString() const
* getErrAsString * getErrAsString
* This function returns error state (ERR) as readable text. * This function returns error state (ERR) as readable text.
*/ */
String VeDirectMpptController::veMpptStruct::getErrAsString() const frozen::string const& VeDirectMpptController::veMpptStruct::getErrAsString() const
{ {
static const std::map<uint8_t, String> values = { static constexpr frozen::map<uint8_t, frozen::string, 20> values = {
{ 0, F("No error") }, { 0, "No error" },
{ 2, F("Battery voltage too high") }, { 2, "Battery voltage too high" },
{ 17, F("Charger temperature too high") }, { 17, "Charger temperature too high" },
{ 18, F("Charger over current") }, { 18, "Charger over current" },
{ 19, F("Charger current reversed") }, { 19, "Charger current reversed" },
{ 20, F("Bulk time limit exceeded") }, { 20, "Bulk time limit exceeded" },
{ 21, F("Current sensor issue(sensor bias/sensor broken)") }, { 21, "Current sensor issue(sensor bias/sensor broken)" },
{ 26, F("Terminals overheated") }, { 26, "Terminals overheated" },
{ 28, F("Converter issue (dual converter models only)") }, { 28, "Converter issue (dual converter models only)" },
{ 33, F("Input voltage too high (solar panel)") }, { 33, "Input voltage too high (solar panel)" },
{ 34, F("Input current too high (solar panel)") }, { 34, "Input current too high (solar panel)" },
{ 38, F("Input shutdown (due to excessive battery voltage)") }, { 38, "Input shutdown (due to excessive battery voltage)" },
{ 39, F("Input shutdown (due to current flow during off mode)") }, { 39, "Input shutdown (due to current flow during off mode)" },
{ 40, F("Input") }, { 40, "Input" },
{ 65, F("Lost communication with one of devices") }, { 65, "Lost communication with one of devices" },
{ 67, F("Synchronisedcharging device configuration issue") }, { 67, "Synchronisedcharging device configuration issue" },
{ 68, F("BMS connection lost") }, { 68, "BMS connection lost" },
{ 116, F("Factory calibration data lost") }, { 116, "Factory calibration data lost" },
{ 117, F("Invalid/incompatible firmware") }, { 117, "Invalid/incompatible firmware" },
{ 118, F("User settings invalid") } { 118, "User settings invalid" }
}; };
return getAsString(values, ERR); return getAsString(values, ERR);
@ -158,19 +157,19 @@ String VeDirectMpptController::veMpptStruct::getErrAsString() const
* getOrAsString * getOrAsString
* This function returns the off reason (OR) as readable text. * This function returns the off reason (OR) as readable text.
*/ */
String VeDirectMpptController::veMpptStruct::getOrAsString() const frozen::string const& VeDirectMpptController::veMpptStruct::getOrAsString() const
{ {
static const std::map<uint32_t, String> values = { static constexpr frozen::map<uint32_t, frozen::string, 10> values = {
{ 0x00000000, F("Not off") }, { 0x00000000, "Not off" },
{ 0x00000001, F("No input power") }, { 0x00000001, "No input power" },
{ 0x00000002, F("Switched off (power switch)") }, { 0x00000002, "Switched off (power switch)" },
{ 0x00000004, F("Switched off (device moderegister)") }, { 0x00000004, "Switched off (device moderegister)" },
{ 0x00000008, F("Remote input") }, { 0x00000008, "Remote input" },
{ 0x00000010, F("Protection active") }, { 0x00000010, "Protection active" },
{ 0x00000020, F("Paygo") }, { 0x00000020, "Paygo" },
{ 0x00000040, F("BMS") }, { 0x00000040, "BMS" },
{ 0x00000080, F("Engine shutdown detection") }, { 0x00000080, "Engine shutdown detection" },
{ 0x00000100, F("Analysing input voltage") } { 0x00000100, "Analysing input voltage" }
}; };
return getAsString(values, OR); return getAsString(values, OR);

View File

@ -59,10 +59,10 @@ public:
double H22; // yield yesterday kWh double H22; // yield yesterday kWh
int32_t H23; // maximum power yesterday W int32_t H23; // maximum power yesterday W
String getMpptAsString() const; // state of mppt as string frozen::string const& getMpptAsString() const; // state of mppt as string
String getCsAsString() const; // current state as string frozen::string const& getCsAsString() const; // current state as string
String getErrAsString() const; // error state as string frozen::string const& getErrAsString() const; // error state as string
String getOrAsString() const; // off reason as string frozen::string const& getOrAsString() const; // off reason as string
}; };
using spData_t = std::shared_ptr<veMpptStruct const>; using spData_t = std::shared_ptr<veMpptStruct const>;

View File

@ -24,6 +24,7 @@ platform = espressif32@6.5.0
build_flags = build_flags =
-DPIOENV=\"$PIOENV\" -DPIOENV=\"$PIOENV\"
-D_TASK_STD_FUNCTION=1 -D_TASK_STD_FUNCTION=1
-D_TASK_THREAD_SAFE=1
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of ; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and ; https://github.com/espressif/arduino-esp32/issues/9044 and
@ -44,7 +45,7 @@ lib_deps =
https://github.com/arkhipenko/TaskScheduler#testing https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/arkhipenko/TaskScheduler#testing https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/coryjfowler/MCP_CAN_lib https://github.com/coryjfowler/MCP_CAN_lib
plerup/EspSoftwareSerial@^8.0.1 plerup/EspSoftwareSerial @ 8.0.1
mobizt/FirebaseJson @ ^3.0.6 mobizt/FirebaseJson @ ^3.0.6
rweather/Crypto@^0.4.0 rweather/Crypto@^0.4.0

View File

@ -277,7 +277,7 @@ void JkBmsBatteryStats::mqttPublish() const
for (auto iter = JkBms::AlarmBitTexts.begin(); iter != JkBms::AlarmBitTexts.end(); ++iter) { for (auto iter = JkBms::AlarmBitTexts.begin(); iter != JkBms::AlarmBitTexts.end(); ++iter) {
auto bit = iter->first; auto bit = iter->first;
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0"; String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/alarms/") + iter->second.c_str(), value); MqttSettings.publish(String("battery/alarms/") + iter->second.data(), value);
} }
} }
@ -286,7 +286,7 @@ void JkBmsBatteryStats::mqttPublish() const
for (auto iter = JkBms::StatusBitTexts.begin(); iter != JkBms::StatusBitTexts.end(); ++iter) { for (auto iter = JkBms::StatusBitTexts.begin(); iter != JkBms::StatusBitTexts.end(); ++iter) {
auto bit = iter->first; auto bit = iter->first;
String value = (*oStatus & static_cast<uint16_t>(bit))?"1":"0"; String value = (*oStatus & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/status/") + iter->second.c_str(), value); MqttSettings.publish(String("battery/status/") + iter->second.data(), value);
} }
} }
@ -338,7 +338,7 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c
_SoC = shuntData.SOC / 10; _SoC = shuntData.SOC / 10;
_voltage = shuntData.V; _voltage = shuntData.V;
_current = shuntData.I; _current = shuntData.I;
_modelName = shuntData.getPidAsString(); _modelName = shuntData.getPidAsString().data();
_chargeCycles = shuntData.H4; _chargeCycles = shuntData.H4;
_timeToGo = shuntData.TTG / 60; _timeToGo = shuntData.TTG / 60;
_chargedEnergy = shuntData.H18 / 100; _chargedEnergy = shuntData.H18 / 100;

View File

@ -4,9 +4,11 @@
*/ */
#include "Configuration.h" #include "Configuration.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include "defaults.h" #include "defaults.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <nvs_flash.h>
CONFIG_T config; CONFIG_T config;
@ -25,6 +27,10 @@ bool ConfigurationClass::write()
DynamicJsonDocument doc(JSON_BUFFER_SIZE); DynamicJsonDocument doc(JSON_BUFFER_SIZE);
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
JsonObject cfg = doc.createNestedObject("cfg"); JsonObject cfg = doc.createNestedObject("cfg");
cfg["version"] = config.Cfg.Version; cfg["version"] = config.Cfg.Version;
cfg["save_count"] = config.Cfg.SaveCount; cfg["save_count"] = config.Cfg.SaveCount;
@ -225,6 +231,11 @@ bool ConfigurationClass::read()
File f = LittleFS.open(CONFIG_FILENAME, "r", false); File f = LittleFS.open(CONFIG_FILENAME, "r", false);
DynamicJsonDocument doc(JSON_BUFFER_SIZE); DynamicJsonDocument doc(JSON_BUFFER_SIZE);
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
// Deserialize the JSON document // Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f); const DeserializationError error = deserializeJson(doc, f);
if (error) { if (error) {
@ -460,6 +471,11 @@ void ConfigurationClass::migrate()
} }
DynamicJsonDocument doc(JSON_BUFFER_SIZE); DynamicJsonDocument doc(JSON_BUFFER_SIZE);
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
// Deserialize the JSON document // Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f); const DeserializationError error = deserializeJson(doc, f);
if (error) { if (error) {
@ -489,6 +505,14 @@ void ConfigurationClass::migrate()
config.Dtu.Nrf.PaLevel = dtu["pa_level"]; config.Dtu.Nrf.PaLevel = dtu["pa_level"];
} }
if (config.Cfg.Version < 0x00011a00) {
// This migration fixes this issue: https://github.com/espressif/arduino-esp32/issues/8828
// It occours when migrating from Core 2.0.9 to 2.0.14
// which was done by updating ESP32 PlatformIO from 6.3.2 to 6.5.0
nvs_flash_erase();
nvs_flash_init();
}
f.close(); f.close();
config.Cfg.Version = CONFIG_VERSION; config.Cfg.Version = CONFIG_VERSION;

View File

@ -5,7 +5,7 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "JkBmsDataPoints.h" #include "JkBmsDataPoints.h"
#include "JkBmsController.h" #include "JkBmsController.h"
#include <map> #include <frozen/map.h>
//#define JKBMS_DUMMY_SERIAL //#define JKBMS_DUMMY_SERIAL
@ -216,7 +216,7 @@ bool Controller::init(bool verboseLogging)
pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen); pin.battery_rx, pin.battery_rxen, pin.battery_tx, pin.battery_txen);
if (pin.battery_rx < 0 || pin.battery_tx < 0) { if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.println(F("[JK BMS] Invalid RX/TX pin config")); MessageOutput.println("[JK BMS] Invalid RX/TX pin config");
return false; return false;
} }
@ -229,7 +229,7 @@ bool Controller::init(bool verboseLogging)
_txEnablePin = pin.battery_txen; _txEnablePin = pin.battery_txen;
if (_rxEnablePin < 0 || _txEnablePin < 0) { if (_rxEnablePin < 0 || _txEnablePin < 0) {
MessageOutput.println(F("[JK BMS] Invalid transceiver pin config")); MessageOutput.println("[JK BMS] Invalid transceiver pin config");
return false; return false;
} }
@ -255,11 +255,11 @@ Controller::Interface Controller::getInterface() const
return Interface::Invalid; return Interface::Invalid;
} }
std::string const& Controller::getStatusText(Controller::Status status) frozen::string const& Controller::getStatusText(Controller::Status status)
{ {
static const std::string missing = "programmer error: missing status text"; static constexpr frozen::string missing = "programmer error: missing status text";
static const std::map<Status, const std::string> texts = { static constexpr frozen::map<Status, frozen::string, 6> texts = {
{ Status::Timeout, "timeout wating for response from BMS" }, { Status::Timeout, "timeout wating for response from BMS" },
{ Status::WaitingForPollInterval, "waiting for poll interval to elapse" }, { Status::WaitingForPollInterval, "waiting for poll interval to elapse" },
{ Status::HwSerialNotAvailableForWrite, "UART is not available for writing" }, { Status::HwSerialNotAvailableForWrite, "UART is not available for writing" },
@ -279,7 +279,7 @@ void Controller::announceStatus(Controller::Status status)
if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; } if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; }
MessageOutput.printf("[%11.3f] JK BMS: %s\r\n", MessageOutput.printf("[%11.3f] JK BMS: %s\r\n",
static_cast<double>(millis())/1000, getStatusText(status).c_str()); static_cast<double>(millis())/1000, getStatusText(status).data());
_lastStatus = status; _lastStatus = status;
_lastStatusPrinted = millis(); _lastStatusPrinted = millis();

View File

@ -8,6 +8,7 @@
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "VictronMppt.h" #include "VictronMppt.h"
#include "Utils.h"
MqttHandleVedirectHassClass MqttHandleVedirectHass; MqttHandleVedirectHassClass MqttHandleVedirectHass;
@ -109,29 +110,32 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
statTopic.concat(subTopic); statTopic.concat(subTopic);
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
root[F("name")] = caption; if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
root[F("stat_t")] = statTopic; return;
root[F("uniq_id")] = serial + "_" + sensorId; }
root["name"] = caption;
root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId;
if (icon != NULL) { if (icon != NULL) {
root[F("icon")] = icon; root["icon"] = icon;
} }
if (unitOfMeasurement != NULL) { if (unitOfMeasurement != NULL) {
root[F("unit_of_meas")] = unitOfMeasurement; root["unit_of_meas"] = unitOfMeasurement;
} }
JsonObject deviceObj = root.createNestedObject("dev"); JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj); createDeviceInfo(deviceObj);
if (Configuration.get().Mqtt.Hass.Expire) { if (Configuration.get().Mqtt.Hass.Expire) {
root[F("exp_aft")] = Configuration.get().Mqtt.PublishInterval * 3; root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
} }
if (deviceClass != NULL) { if (deviceClass != NULL) {
root[F("dev_cla")] = deviceClass; root["dev_cla"] = deviceClass;
} }
if (stateClass != NULL) { if (stateClass != NULL) {
root[F("stat_cla")] = stateClass; root["stat_cla"] = stateClass;
} }
char buffer[512]; char buffer[512];
@ -160,14 +164,17 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
statTopic.concat(subTopic); statTopic.concat(subTopic);
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
root[F("name")] = caption; if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
root[F("uniq_id")] = serial + "_" + sensorId; return;
root[F("stat_t")] = statTopic; }
root[F("pl_on")] = payload_on; root["name"] = caption;
root[F("pl_off")] = payload_off; root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (icon != NULL) { if (icon != NULL) {
root[F("icon")] = icon; root["icon"] = icon;
} }
JsonObject deviceObj = root.createNestedObject("dev"); JsonObject deviceObj = root.createNestedObject("dev");
@ -182,12 +189,12 @@ void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
{ {
auto spMpptData = VictronMppt.getData(); auto spMpptData = VictronMppt.getData();
String serial = spMpptData->SER; String serial = spMpptData->SER;
object[F("name")] = "Victron(" + serial + ")"; object["name"] = "Victron(" + serial + ")";
object[F("ids")] = serial; object["ids"] = serial;
object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); object["cu"] = String("http://") + NetworkSettings.localIP().toString();
object[F("mf")] = F("OpenDTU"); object["mf"] = "OpenDTU";
object[F("mdl")] = spMpptData->getPidAsString(); object["mdl"] = spMpptData->getPidAsString();
object[F("sw")] = AUTO_GIT_HASH; object["sw"] = AUTO_GIT_HASH;
} }
void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload) void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload)

View File

@ -135,6 +135,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
} }
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["stat_t"] = stateTopic; root["stat_t"] = stateTopic;
root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName; root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName;
@ -179,6 +183,10 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) { if (strcmp(icon, "")) {
@ -217,6 +225,10 @@ void MqttHandleHassClass::publishInverterNumber(
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) { if (strcmp(icon, "")) {
@ -251,6 +263,10 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId; root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic; root["stat_t"] = statTopic;
@ -275,6 +291,10 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
} }
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id; root["uniq_id"] = getDtuUniqueId() + "_" + id;
if (strcmp(device_class, "")) { if (strcmp(device_class, "")) {
@ -317,6 +337,10 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
} }
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id; root["uniq_id"] = getDtuUniqueId() + "_" + id;
root["stat_t"] = MqttSettings.getPrefix() + topic; root["stat_t"] = MqttSettings.getPrefix() + topic;

View File

@ -6,6 +6,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "MqttSettings.h" #include "MqttSettings.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
MqttHandlePylontechHassClass MqttHandlePylontechHass; MqttHandlePylontechHassClass MqttHandlePylontechHass;
@ -115,6 +116,9 @@ void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char
statTopic.concat(subTopic); statTopic.concat(subTopic);
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["stat_t"] = statTopic; root["stat_t"] = statTopic;
root["uniq_id"] = serial + "_" + sensorId; root["uniq_id"] = serial + "_" + sensorId;
@ -166,6 +170,9 @@ void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, cons
statTopic.concat(subTopic); statTopic.concat(subTopic);
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId; root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic; root["stat_t"] = statTopic;

View File

@ -69,7 +69,7 @@ void MqttHandleVedirectClass::loop()
topic.concat("/"); topic.concat("/");
if (_PublishFull || spMpptData->PID != _kvFrame.PID) if (_PublishFull || spMpptData->PID != _kvFrame.PID)
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString()); MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0) if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0)
MqttSettings.publish(topic + "SER", spMpptData->SER ); MqttSettings.publish(topic + "SER", spMpptData->SER );
if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0) if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0)
@ -77,13 +77,13 @@ void MqttHandleVedirectClass::loop()
if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD) if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD)
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF"); MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF");
if (_PublishFull || spMpptData->CS != _kvFrame.CS) if (_PublishFull || spMpptData->CS != _kvFrame.CS)
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString()); MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
if (_PublishFull || spMpptData->ERR != _kvFrame.ERR) if (_PublishFull || spMpptData->ERR != _kvFrame.ERR)
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString()); MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
if (_PublishFull || spMpptData->OR != _kvFrame.OR) if (_PublishFull || spMpptData->OR != _kvFrame.OR)
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString()); MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT) if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT)
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString()); MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) { if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) {
value = spMpptData->HSDS; value = spMpptData->HSDS;
MqttSettings.publish(topic + "HSDS", value); MqttSettings.publish(topic + "HSDS", value);

View File

@ -268,7 +268,8 @@ void NetworkSettingsClass::applyConfig()
MessageOutput.print("new credentials... "); MessageOutput.print("new credentials... ");
WiFi.begin( WiFi.begin(
Configuration.get().WiFi.Ssid, Configuration.get().WiFi.Ssid,
Configuration.get().WiFi.Password); Configuration.get().WiFi.Password,
WIFI_ALL_CHANNEL_SCAN);
} else { } else {
MessageOutput.print("existing credentials... "); MessageOutput.print("existing credentials... ");
WiFi.begin(); WiFi.begin();

View File

@ -14,7 +14,7 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include <ctime> #include <ctime>
#include <cmath> #include <cmath>
#include <map> #include <frozen/map.h>
PowerLimiterClass PowerLimiter; PowerLimiterClass PowerLimiter;
@ -26,11 +26,11 @@ void PowerLimiterClass::init(Scheduler& scheduler)
_loopTask.enable(); _loopTask.enable();
} }
std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status) frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status)
{ {
static const std::string missing = "programmer error: missing status text"; static const frozen::string missing = "programmer error: missing status text";
static const std::map<Status, const std::string> texts = { static const frozen::map<Status, frozen::string, 19> texts = {
{ Status::Initializing, "initializing (should not see me)" }, { Status::Initializing, "initializing (should not see me)" },
{ Status::DisabledByConfig, "disabled by configuration" }, { Status::DisabledByConfig, "disabled by configuration" },
{ Status::DisabledByMqtt, "disabled by MQTT" }, { Status::DisabledByMqtt, "disabled by MQTT" },
@ -70,7 +70,7 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status)
if (status == Status::DisabledByConfig && _lastStatus == status) { return; } if (status == Status::DisabledByConfig && _lastStatus == status) { return; }
MessageOutput.printf("[%11.3f] DPL: %s\r\n", MessageOutput.printf("[%11.3f] DPL: %s\r\n",
static_cast<double>(millis())/1000, getStatusText(status).c_str()); static_cast<double>(millis())/1000, getStatusText(status).data());
_lastStatus = status; _lastStatus = status;
_lastStatusPrinted = millis(); _lastStatusPrinted = millis();
@ -586,7 +586,7 @@ float PowerLimiterClass::getLoadCorrectedVoltage()
{ {
if (!_inverter) { if (!_inverter) {
// there should be no need to call this method if no target inverter is known // there should be no need to call this method if no target inverter is known
MessageOutput.println(F("DPL getLoadCorrectedVoltage: no inverter (programmer error)")); MessageOutput.println("DPL getLoadCorrectedVoltage: no inverter (programmer error)");
return 0.0; return 0.0;
} }

View File

@ -12,14 +12,14 @@ bool PylontechCanReceiver::init(bool verboseLogging)
{ {
_verboseLogging = verboseLogging; _verboseLogging = verboseLogging;
MessageOutput.println(F("[Pylontech] Initialize interface...")); MessageOutput.println("[Pylontech] Initialize interface...");
const PinMapping_t& pin = PinMapping.get(); const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[Pylontech] Interface rx = %d, tx = %d\r\n", MessageOutput.printf("[Pylontech] Interface rx = %d, tx = %d\r\n",
pin.battery_rx, pin.battery_tx); pin.battery_rx, pin.battery_tx);
if (pin.battery_rx < 0 || pin.battery_tx < 0) { if (pin.battery_rx < 0 || pin.battery_tx < 0) {
MessageOutput.println(F("[Pylontech] Invalid pin config")); MessageOutput.println("[Pylontech] Invalid pin config");
return false; return false;
} }
@ -35,18 +35,18 @@ bool PylontechCanReceiver::init(bool verboseLogging)
esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config); esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config);
switch (twaiLastResult) { switch (twaiLastResult) {
case ESP_OK: case ESP_OK:
MessageOutput.println(F("[Pylontech] Twai driver installed")); MessageOutput.println("[Pylontech] Twai driver installed");
break; break;
case ESP_ERR_INVALID_ARG: case ESP_ERR_INVALID_ARG:
MessageOutput.println(F("[Pylontech] Twai driver install - invalid arg")); MessageOutput.println("[Pylontech] Twai driver install - invalid arg");
return false; return false;
break; break;
case ESP_ERR_NO_MEM: case ESP_ERR_NO_MEM:
MessageOutput.println(F("[Pylontech] Twai driver install - no memory")); MessageOutput.println("[Pylontech] Twai driver install - no memory");
return false; return false;
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
MessageOutput.println(F("[Pylontech] Twai driver install - invalid state")); MessageOutput.println("[Pylontech] Twai driver install - invalid state");
return false; return false;
break; break;
} }
@ -55,10 +55,10 @@ bool PylontechCanReceiver::init(bool verboseLogging)
twaiLastResult = twai_start(); twaiLastResult = twai_start();
switch (twaiLastResult) { switch (twaiLastResult) {
case ESP_OK: case ESP_OK:
MessageOutput.println(F("[Pylontech] Twai driver started")); MessageOutput.println("[Pylontech] Twai driver started");
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
MessageOutput.println(F("[Pylontech] Twai driver start - invalid state")); MessageOutput.println("[Pylontech] Twai driver start - invalid state");
return false; return false;
break; break;
} }
@ -72,10 +72,10 @@ void PylontechCanReceiver::deinit()
esp_err_t twaiLastResult = twai_stop(); esp_err_t twaiLastResult = twai_stop();
switch (twaiLastResult) { switch (twaiLastResult) {
case ESP_OK: case ESP_OK:
MessageOutput.println(F("[Pylontech] Twai driver stopped")); MessageOutput.println("[Pylontech] Twai driver stopped");
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
MessageOutput.println(F("[Pylontech] Twai driver stop - invalid state")); MessageOutput.println("[Pylontech] Twai driver stop - invalid state");
break; break;
} }
@ -83,10 +83,10 @@ void PylontechCanReceiver::deinit()
twaiLastResult = twai_driver_uninstall(); twaiLastResult = twai_driver_uninstall();
switch (twaiLastResult) { switch (twaiLastResult) {
case ESP_OK: case ESP_OK:
MessageOutput.println(F("[Pylontech] Twai driver uninstalled")); MessageOutput.println("[Pylontech] Twai driver uninstalled");
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
MessageOutput.println(F("[Pylontech] Twai driver uninstall - invalid state")); MessageOutput.println("[Pylontech] Twai driver uninstall - invalid state");
break; break;
} }
} }
@ -103,10 +103,10 @@ void PylontechCanReceiver::loop()
if (twaiLastResult != ESP_OK) { if (twaiLastResult != ESP_OK) {
switch (twaiLastResult) { switch (twaiLastResult) {
case ESP_ERR_INVALID_ARG: case ESP_ERR_INVALID_ARG:
MessageOutput.println(F("[Pylontech] Twai driver get status - invalid arg")); MessageOutput.println("[Pylontech] Twai driver get status - invalid arg");
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
MessageOutput.println(F("[Pylontech] Twai driver get status - invalid state")); MessageOutput.println("[Pylontech] Twai driver get status - invalid state");
break; break;
} }
return; return;
@ -118,7 +118,7 @@ void PylontechCanReceiver::loop()
// Wait for message to be received, function is blocking // Wait for message to be received, function is blocking
twai_message_t rx_message; twai_message_t rx_message;
if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) { if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) {
MessageOutput.println(F("[Pylontech] Failed to receive message")); MessageOutput.println("[Pylontech] Failed to receive message");
return; return;
} }

View File

@ -5,6 +5,7 @@
#include "Utils.h" #include "Utils.h"
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "Led_Single.h" #include "Led_Single.h"
#include "MessageOutput.h"
#include <Esp.h> #include <Esp.h>
uint32_t Utils::getChipId() uint32_t Utils::getChipId()
@ -65,3 +66,13 @@ void Utils::restartDtu()
yield(); yield();
ESP.restart(); ESP.restart();
} }
bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line)
{
if (doc.capacity() == 0) {
MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line);
return false;
}
return true;
}

View File

@ -32,7 +32,7 @@ void VictronMpptClass::updateSettings()
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx); MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx);
if (rx < 0) { if (rx < 0) {
MessageOutput.println(F("[VictronMppt] invalid pin config")); MessageOutput.println("[VictronMppt] invalid pin config");
return; return;
} }

View File

@ -7,14 +7,14 @@
bool VictronSmartShunt::init(bool verboseLogging) bool VictronSmartShunt::init(bool verboseLogging)
{ {
MessageOutput.println(F("[VictronSmartShunt] Initialize interface...")); MessageOutput.println("[VictronSmartShunt] Initialize interface...");
const PinMapping_t& pin = PinMapping.get(); const PinMapping_t& pin = PinMapping.get();
MessageOutput.printf("[VictronSmartShunt] Interface rx = %d, tx = %d\r\n", MessageOutput.printf("[VictronSmartShunt] Interface rx = %d, tx = %d\r\n",
pin.battery_rx, pin.battery_tx); pin.battery_rx, pin.battery_tx);
if (pin.battery_rx < 0) { if (pin.battery_rx < 0) {
MessageOutput.println(F("[VictronSmartShunt] Invalid pin config")); MessageOutput.println("[VictronSmartShunt] Invalid pin config");
return false; return false;
} }
@ -28,5 +28,9 @@ bool VictronSmartShunt::init(bool verboseLogging)
void VictronSmartShunt::loop() void VictronSmartShunt::loop()
{ {
VeDirectShunt.loop(); VeDirectShunt.loop();
if (VeDirectShunt.getLastUpdate() <= _lastUpdate) { return; }
_stats->updateFrom(VeDirectShunt.veFrame); _stats->updateFrom(VeDirectShunt.veFrame);
_lastUpdate = VeDirectShunt.getLastUpdate();
} }

View File

@ -117,4 +117,16 @@ void WebApiClass::sendTooManyRequests(AsyncWebServerRequest* request)
request->send(response); request->send(response);
} }
void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const String& message)
{
if (!Configuration.write()) {
retMsg["message"] = "Write failed!";
retMsg["code"] = WebApiError::GenericWriteFailed;
} else {
retMsg["type"] = "success";
retMsg["message"] = message;
retMsg["code"] = code;
}
}
WebApiClass WebApi; WebApiClass WebApi;

View File

@ -28,7 +28,7 @@ void WebApiHuaweiClass::loop()
{ {
} }
void WebApiHuaweiClass::getJsonData(JsonObject& root) { void WebApiHuaweiClass::getJsonData(JsonVariant& root) {
const RectifierParameters_t * rp = HuaweiCan.get(); const RectifierParameters_t * rp = HuaweiCan.get();
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
@ -62,7 +62,7 @@ void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
getJsonData(root); getJsonData(root);
response->setLength(); response->setLength();
@ -76,7 +76,7 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -186,7 +186,7 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["enabled"] = config.Huawei.Enabled; root["enabled"] = config.Huawei.Enabled;
@ -208,7 +208,7 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -261,11 +261,8 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>(); config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as<float>();
config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>(); config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as<float>();
config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>(); config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as<float>();
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -35,15 +35,15 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.Battery.Enabled; root["enabled"] = config.Battery.Enabled;
root[F("verbose_logging")] = config.Battery.VerboseLogging; root["verbose_logging"] = config.Battery.VerboseLogging;
root[F("provider")] = config.Battery.Provider; root["provider"] = config.Battery.Provider;
root[F("jkbms_interface")] = config.Battery.JkBmsInterface; root["jkbms_interface"] = config.Battery.JkBmsInterface;
root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval; root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
root[F("mqtt_topic")] = config.Battery.MqttTopic; root["mqtt_topic"] = config.Battery.MqttTopic;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -61,12 +61,12 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!"); retMsg["message"] = "No values found!";
retMsg[F("code")] = WebApiError::GenericNoValueFound; retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -75,8 +75,8 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
String json = request->getParam("data", true)->value(); String json = request->getParam("data", true)->value();
if (json.length() > 1024) { if (json.length() > 1024) {
retMsg[F("message")] = F("Data too large!"); retMsg["message"] = "Data too large!";
retMsg[F("code")] = WebApiError::GenericDataTooLarge; retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -86,33 +86,30 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
DeserializationError error = deserializeJson(root, json); DeserializationError error = deserializeJson(root, json);
if (error) { if (error) {
retMsg[F("message")] = F("Failed to parse data!"); retMsg["message"] = "Failed to parse data!";
retMsg[F("code")] = WebApiError::GenericParseError; retMsg["code"] = WebApiError::GenericParseError;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
if (!root.containsKey(F("enabled")) || !root.containsKey(F("provider"))) { if (!root.containsKey("enabled") || !root.containsKey("provider")) {
retMsg[F("message")] = F("Values are missing!"); retMsg["message"] = "Values are missing!";
retMsg[F("code")] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.Battery.Enabled = root[F("enabled")].as<bool>(); config.Battery.Enabled = root["enabled"].as<bool>();
config.Battery.VerboseLogging = root[F("verbose_logging")].as<bool>(); config.Battery.VerboseLogging = root["verbose_logging"].as<bool>();
config.Battery.Provider = root[F("provider")].as<uint8_t>(); config.Battery.Provider = root["provider"].as<uint8_t>();
config.Battery.JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>(); config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].as<uint8_t>(); config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as<uint8_t>();
strlcpy(config.Battery.MqttTopic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Battery.MqttTopic)); strlcpy(config.Battery.MqttTopic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Battery.MqttTopic));
Configuration.write();
retMsg[F("type")] = F("success"); WebApi.writeConfig(retMsg);
retMsg[F("message")] = F("Settings saved!");
retMsg[F("code")] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -59,7 +59,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -125,8 +125,8 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
JsonArray data = root.createNestedArray("configs"); auto data = root.createNestedArray("configs");
File rootfs = LittleFS.open("/"); File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile(); File file = rootfs.openNextFile();

View File

@ -33,14 +33,14 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
const PinMapping_t& pin = PinMapping.get(); const PinMapping_t& pin = PinMapping.get();
JsonObject curPin = root.createNestedObject("curPin"); auto curPin = root.createNestedObject("curPin");
curPin["name"] = config.Dev_PinMapping; curPin["name"] = config.Dev_PinMapping;
JsonObject nrfPinObj = curPin.createNestedObject("nrf24"); auto nrfPinObj = curPin.createNestedObject("nrf24");
nrfPinObj["clk"] = pin.nrf24_clk; nrfPinObj["clk"] = pin.nrf24_clk;
nrfPinObj["cs"] = pin.nrf24_cs; nrfPinObj["cs"] = pin.nrf24_cs;
nrfPinObj["en"] = pin.nrf24_en; nrfPinObj["en"] = pin.nrf24_en;
@ -48,7 +48,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
nrfPinObj["miso"] = pin.nrf24_miso; nrfPinObj["miso"] = pin.nrf24_miso;
nrfPinObj["mosi"] = pin.nrf24_mosi; nrfPinObj["mosi"] = pin.nrf24_mosi;
JsonObject cmtPinObj = curPin.createNestedObject("cmt"); auto cmtPinObj = curPin.createNestedObject("cmt");
cmtPinObj["clk"] = pin.cmt_clk; cmtPinObj["clk"] = pin.cmt_clk;
cmtPinObj["cs"] = pin.cmt_cs; cmtPinObj["cs"] = pin.cmt_cs;
cmtPinObj["fcs"] = pin.cmt_fcs; cmtPinObj["fcs"] = pin.cmt_fcs;
@ -56,7 +56,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio2"] = pin.cmt_gpio2;
cmtPinObj["gpio3"] = pin.cmt_gpio3; cmtPinObj["gpio3"] = pin.cmt_gpio3;
JsonObject ethPinObj = curPin.createNestedObject("eth"); auto ethPinObj = curPin.createNestedObject("eth");
ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["enabled"] = pin.eth_enabled;
ethPinObj["phy_addr"] = pin.eth_phy_addr; ethPinObj["phy_addr"] = pin.eth_phy_addr;
ethPinObj["power"] = pin.eth_power; ethPinObj["power"] = pin.eth_power;
@ -65,19 +65,19 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
ethPinObj["type"] = pin.eth_type; ethPinObj["type"] = pin.eth_type;
ethPinObj["clk_mode"] = pin.eth_clk_mode; ethPinObj["clk_mode"] = pin.eth_clk_mode;
JsonObject displayPinObj = curPin.createNestedObject("display"); auto displayPinObj = curPin.createNestedObject("display");
displayPinObj["type"] = pin.display_type; displayPinObj["type"] = pin.display_type;
displayPinObj["data"] = pin.display_data; displayPinObj["data"] = pin.display_data;
displayPinObj["clk"] = pin.display_clk; displayPinObj["clk"] = pin.display_clk;
displayPinObj["cs"] = pin.display_cs; displayPinObj["cs"] = pin.display_cs;
displayPinObj["reset"] = pin.display_reset; displayPinObj["reset"] = pin.display_reset;
JsonObject ledPinObj = curPin.createNestedObject("led"); auto ledPinObj = curPin.createNestedObject("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
ledPinObj["led" + String(i)] = pin.led[i]; ledPinObj["led" + String(i)] = pin.led[i];
} }
JsonObject display = root.createNestedObject("display"); auto display = root.createNestedObject("display");
display["rotation"] = config.Display.Rotation; display["rotation"] = config.Display.Rotation;
display["power_safe"] = config.Display.PowerSafe; display["power_safe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver; display["screensaver"] = config.Display.ScreenSaver;
@ -85,9 +85,9 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["language"] = config.Display.Language; display["language"] = config.Display.Language;
display["diagramduration"] = config.Display.DiagramDuration; display["diagramduration"] = config.Display.DiagramDuration;
JsonArray leds = root.createNestedArray("led"); auto leds = root.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds.createNestedObject(); auto led = leds.createNestedObject();
led["brightness"] = config.Led_Single[i].Brightness; led["brightness"] = config.Led_Single[i].Brightness;
} }
@ -120,7 +120,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -193,11 +193,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
Display.setLanguage(config.Display.Language); Display.setLanguage(config.Display.Language);
Display.Diagram().updatePeriod(); Display.Diagram().updatePeriod();
Configuration.write(); WebApi.writeConfig(retMsg);
retMsg["type"] = "success";
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -28,7 +28,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
uint64_t serial = 0; uint64_t serial = 0;
if (request->hasParam("inv")) { if (request->hasParam("inv")) {

View File

@ -30,7 +30,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// DTU Serial is read as HEX // DTU Serial is read as HEX
@ -58,7 +58,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -157,11 +157,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>(); config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>(); config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>(); config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -27,7 +27,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
uint64_t serial = 0; uint64_t serial = 0;
if (request->hasParam("inv")) { if (request->hasParam("inv")) {

View File

@ -28,7 +28,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
uint64_t serial = 0; uint64_t serial = 0;
if (request->hasParam("inv")) { if (request->hasParam("inv")) {
@ -72,7 +72,7 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
uint64_t serial = 0; uint64_t serial = 0;
if (request->hasParam("inv")) { if (request->hasParam("inv")) {

View File

@ -36,7 +36,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
JsonArray data = root.createNestedArray("inverter"); JsonArray data = root.createNestedArray("inverter");
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -94,7 +94,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -167,11 +167,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16); inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN); strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!");
retMsg["message"] = "Inverter created!";
retMsg["code"] = WebApiError::InverterAdded;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -194,7 +191,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -294,11 +291,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
arrayCount++; arrayCount++;
} }
Configuration.write(); WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!");
retMsg["type"] = "success";
retMsg["code"] = WebApiError::InverterChanged;
retMsg["message"] = "Inverter changed!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -340,7 +333,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -395,11 +388,8 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
inverter.Serial = 0; inverter.Serial = 0;
strncpy(inverter.Name, "", sizeof(inverter.Name)); strncpy(inverter.Name, "", sizeof(inverter.Name));
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
retMsg["message"] = "Inverter deleted!";
retMsg["code"] = WebApiError::InverterDeleted;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -414,7 +404,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -466,11 +456,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
order++; order++;
} }
Configuration.write(); WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");
retMsg["type"] = "success";
retMsg["message"] = "Inverter order saved!";
retMsg["code"] = WebApiError::InverterOrdered;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -31,7 +31,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
@ -64,7 +64,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {

View File

@ -29,7 +29,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {

View File

@ -37,7 +37,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["mqtt_enabled"] = config.Mqtt.Enabled; root["mqtt_enabled"] = config.Mqtt.Enabled;
@ -72,7 +72,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["mqtt_enabled"] = config.Mqtt.Enabled; root["mqtt_enabled"] = config.Mqtt.Enabled;
@ -111,7 +111,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -342,11 +342,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
config.Mqtt.Hass.Retain = root["mqtt_hass_retain"].as<bool>(); config.Mqtt.Hass.Retain = root["mqtt_hass_retain"].as<bool>();
config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as<bool>(); config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as<bool>();
strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as<String>().c_str(), sizeof(config.Mqtt.Hass.Topic)); strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as<String>().c_str(), sizeof(config.Mqtt.Hass.Topic));
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -32,7 +32,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
root["sta_status"] = ((WiFi.getMode() & WIFI_STA) != 0); root["sta_status"] = ((WiFi.getMode() & WIFI_STA) != 0);
root["sta_ssid"] = WiFi.SSID(); root["sta_ssid"] = WiFi.SSID();
@ -63,7 +63,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["hostname"] = config.WiFi.Hostname; root["hostname"] = config.WiFi.Hostname;
@ -89,7 +89,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -238,11 +238,8 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
} }
config.WiFi.ApTimeout = root["aptimeout"].as<uint>(); config.WiFi.ApTimeout = root["aptimeout"].as<uint>();
config.Mdns.Enabled = root["mdnsenabled"].as<bool>(); config.Mdns.Enabled = root["mdnsenabled"].as<bool>();
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);

View File

@ -35,7 +35,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["ntp_server"] = config.Ntp.Server; root["ntp_server"] = config.Ntp.Server;
@ -80,7 +80,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["ntp_server"] = config.Ntp.Server; root["ntp_server"] = config.Ntp.Server;
@ -101,7 +101,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -179,11 +179,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
config.Ntp.Latitude = root["latitude"].as<double>(); config.Ntp.Latitude = root["latitude"].as<double>();
config.Ntp.Longitude = root["longitude"].as<double>(); config.Ntp.Longitude = root["longitude"].as<double>();
config.Ntp.SunsetType = root["sunsettype"].as<uint8_t>(); config.Ntp.SunsetType = root["sunsettype"].as<uint8_t>();
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -201,7 +198,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 5)) { if (!getLocalTime(&timeinfo, 5)) {
@ -228,7 +225,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {

View File

@ -29,7 +29,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
@ -57,7 +57,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {

View File

@ -34,30 +34,30 @@ void WebApiPowerLimiterClass::loop()
void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.PowerLimiter.Enabled; root["enabled"] = config.PowerLimiter.Enabled;
root[F("verbose_logging")] = config.PowerLimiter.VerboseLogging; root["verbose_logging"] = config.PowerLimiter.VerboseLogging;
root[F("solar_passthrough_enabled")] = config.PowerLimiter.SolarPassThroughEnabled; root["solar_passthrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
root[F("solar_passthrough_losses")] = config.PowerLimiter.SolarPassThroughLosses; root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
root[F("battery_drain_strategy")] = config.PowerLimiter.BatteryDrainStategy; root["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
root[F("is_inverter_behind_powermeter")] = config.PowerLimiter.IsInverterBehindPowerMeter; root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
root[F("inverter_id")] = config.PowerLimiter.InverterId; root["inverter_id"] = config.PowerLimiter.InverterId;
root[F("inverter_channel_id")] = config.PowerLimiter.InverterChannelId; root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
root[F("target_power_consumption")] = config.PowerLimiter.TargetPowerConsumption; root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
root[F("target_power_consumption_hysteresis")] = config.PowerLimiter.TargetPowerConsumptionHysteresis; root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
root[F("lower_power_limit")] = config.PowerLimiter.LowerPowerLimit; root["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
root[F("upper_power_limit")] = config.PowerLimiter.UpperPowerLimit; root["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
root[F("battery_soc_start_threshold")] = config.PowerLimiter.BatterySocStartThreshold; root["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
root[F("battery_soc_stop_threshold")] = config.PowerLimiter.BatterySocStopThreshold; root["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
root[F("voltage_start_threshold")] = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100 +0.5) / 100.0; root["voltage_start_threshold"] = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100 +0.5) / 100.0;
root[F("voltage_stop_threshold")] = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100 +0.5) / 100.0;; root["voltage_stop_threshold"] = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100 +0.5) / 100.0;;
root[F("voltage_load_correction_factor")] = config.PowerLimiter.VoltageLoadCorrectionFactor; root["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
root[F("inverter_restart_hour")] = config.PowerLimiter.RestartHour; root["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
root[F("full_solar_passthrough_soc")] = config.PowerLimiter.FullSolarPassThroughSoc; root["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
root[F("full_solar_passthrough_start_voltage")] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0; root["full_solar_passthrough_start_voltage"] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0;
root[F("full_solar_passthrough_stop_voltage")] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0; root["full_solar_passthrough_stop_voltage"] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -79,11 +79,11 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!"); retMsg["message"] = "No values found!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -92,7 +92,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
String json = request->getParam("data", true)->value(); String json = request->getParam("data", true)->value();
if (json.length() > 1024) { if (json.length() > 1024) {
retMsg[F("message")] = F("Data too large!"); retMsg["message"] = "Data too large!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -102,7 +102,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
DeserializationError error = deserializeJson(root, json); DeserializationError error = deserializeJson(root, json);
if (error) { if (error) {
retMsg[F("message")] = F("Failed to parse data!"); retMsg["message"] = "Failed to parse data!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -115,8 +115,8 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("target_power_consumption") && root.containsKey("target_power_consumption")
&& root.containsKey("target_power_consumption_hysteresis") && root.containsKey("target_power_consumption_hysteresis")
)) { )) {
retMsg[F("message")] = F("Values are missing!"); retMsg["message"] = "Values are missing!";
retMsg[F("code")] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -124,40 +124,35 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.PowerLimiter.Enabled = root[F("enabled")].as<bool>(); config.PowerLimiter.Enabled = root["enabled"].as<bool>();
PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation
config.PowerLimiter.VerboseLogging = root[F("verbose_logging")].as<bool>(); config.PowerLimiter.VerboseLogging = root["verbose_logging"].as<bool>();
config.PowerLimiter.SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as<bool>(); config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as<bool>();
config.PowerLimiter.SolarPassThroughLosses = root[F("solar_passthrough_losses")].as<uint8_t>(); config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as<uint8_t>();
config.PowerLimiter.BatteryDrainStategy= root[F("battery_drain_strategy")].as<uint8_t>(); config.PowerLimiter.BatteryDrainStategy= root["battery_drain_strategy"].as<uint8_t>();
config.PowerLimiter.IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>(); config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as<bool>();
config.PowerLimiter.InverterId = root[F("inverter_id")].as<uint8_t>(); config.PowerLimiter.InverterId = root["inverter_id"].as<uint8_t>();
config.PowerLimiter.InverterChannelId = root[F("inverter_channel_id")].as<uint8_t>(); config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as<uint8_t>();
config.PowerLimiter.TargetPowerConsumption = root[F("target_power_consumption")].as<int32_t>(); config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();
config.PowerLimiter.TargetPowerConsumptionHysteresis = root[F("target_power_consumption_hysteresis")].as<int32_t>(); config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as<int32_t>();
config.PowerLimiter.LowerPowerLimit = root[F("lower_power_limit")].as<int32_t>(); config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as<int32_t>();
config.PowerLimiter.UpperPowerLimit = root[F("upper_power_limit")].as<int32_t>(); config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();
config.PowerLimiter.BatterySocStartThreshold = root[F("battery_soc_start_threshold")].as<uint32_t>(); config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as<uint32_t>();
config.PowerLimiter.BatterySocStopThreshold = root[F("battery_soc_stop_threshold")].as<uint32_t>(); config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as<uint32_t>();
config.PowerLimiter.VoltageStartThreshold = root[F("voltage_start_threshold")].as<float>(); config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as<float>();
config.PowerLimiter.VoltageStartThreshold = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0; config.PowerLimiter.VoltageStartThreshold = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0;
config.PowerLimiter.VoltageStopThreshold = root[F("voltage_stop_threshold")].as<float>(); config.PowerLimiter.VoltageStopThreshold = root["voltage_stop_threshold"].as<float>();
config.PowerLimiter.VoltageStopThreshold = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0; config.PowerLimiter.VoltageStopThreshold = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0;
config.PowerLimiter.VoltageLoadCorrectionFactor = root[F("voltage_load_correction_factor")].as<float>(); config.PowerLimiter.VoltageLoadCorrectionFactor = root["voltage_load_correction_factor"].as<float>();
config.PowerLimiter.RestartHour = root[F("inverter_restart_hour")].as<int8_t>(); config.PowerLimiter.RestartHour = root["inverter_restart_hour"].as<int8_t>();
config.PowerLimiter.FullSolarPassThroughSoc = root[F("full_solar_passthrough_soc")].as<uint32_t>(); config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as<uint32_t>();
config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast<int>(root[F("full_solar_passthrough_start_voltage")].as<float>() * 100) / 100.0; config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast<int>(root["full_solar_passthrough_start_voltage"].as<float>() * 100) / 100.0;
config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast<int>(root[F("full_solar_passthrough_stop_voltage")].as<float>() * 100) / 100.0; config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast<int>(root["full_solar_passthrough_stop_voltage"].as<float>() * 100) / 100.0;
WebApi.writeConfig(retMsg);
Configuration.write();
PowerLimiter.calcNextInverterRestart();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
response->setLength(); response->setLength();
request->send(response); request->send(response);
PowerLimiter.calcNextInverterRestart();
} }

View File

@ -35,35 +35,35 @@ void WebApiPowerMeterClass::loop()
void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("enabled")] = config.PowerMeter.Enabled; root["enabled"] = config.PowerMeter.Enabled;
root[F("verbose_logging")] = config.PowerMeter.VerboseLogging; root["verbose_logging"] = config.PowerMeter.VerboseLogging;
root[F("source")] = config.PowerMeter.Source; root["source"] = config.PowerMeter.Source;
root[F("interval")] = config.PowerMeter.Interval; root["interval"] = config.PowerMeter.Interval;
root[F("mqtt_topic_powermeter_1")] = config.PowerMeter.MqttTopicPowerMeter1; root["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1;
root[F("mqtt_topic_powermeter_2")] = config.PowerMeter.MqttTopicPowerMeter2; root["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2;
root[F("mqtt_topic_powermeter_3")] = config.PowerMeter.MqttTopicPowerMeter3; root["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3;
root[F("sdmbaudrate")] = config.PowerMeter.SdmBaudrate; root["sdmbaudrate"] = config.PowerMeter.SdmBaudrate;
root[F("sdmaddress")] = config.PowerMeter.SdmAddress; root["sdmaddress"] = config.PowerMeter.SdmAddress;
root[F("http_individual_requests")] = config.PowerMeter.HttpIndividualRequests; root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
JsonArray httpPhases = root.createNestedArray(F("http_phases")); JsonArray httpPhases = root.createNestedArray("http_phases");
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject phaseObject = httpPhases.createNestedObject(); JsonObject phaseObject = httpPhases.createNestedObject();
phaseObject[F("index")] = i + 1; phaseObject["index"] = i + 1;
phaseObject[F("enabled")] = config.PowerMeter.Http_Phase[i].Enabled; phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
phaseObject[F("url")] = String(config.PowerMeter.Http_Phase[i].Url); phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url);
phaseObject[F("auth_type")]= config.PowerMeter.Http_Phase[i].AuthType; phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType;
phaseObject[F("username")] = String(config.PowerMeter.Http_Phase[i].Username); phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username);
phaseObject[F("password")] = String(config.PowerMeter.Http_Phase[i].Password); phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password);
phaseObject[F("header_key")] = String(config.PowerMeter.Http_Phase[i].HeaderKey); phaseObject["header_key"] = String(config.PowerMeter.Http_Phase[i].HeaderKey);
phaseObject[F("header_value")] = String(config.PowerMeter.Http_Phase[i].HeaderValue); phaseObject["header_value"] = String(config.PowerMeter.Http_Phase[i].HeaderValue);
phaseObject[F("json_path")] = String(config.PowerMeter.Http_Phase[i].JsonPath); phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath);
phaseObject[F("timeout")] = config.PowerMeter.Http_Phase[i].Timeout; phaseObject["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
} }
response->setLength(); response->setLength();
@ -86,11 +86,11 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!"); retMsg["message"] = "No values found!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -99,7 +99,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
String json = request->getParam("data", true)->value(); String json = request->getParam("data", true)->value();
if (json.length() > 4096) { if (json.length() > 4096) {
retMsg[F("message")] = F("Data too large!"); retMsg["message"] = "Data too large!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -109,49 +109,49 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
DeserializationError error = deserializeJson(root, json); DeserializationError error = deserializeJson(root, json);
if (error) { if (error) {
retMsg[F("message")] = F("Failed to parse data!"); retMsg["message"] = "Failed to parse data!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
if (!(root.containsKey("enabled") && root.containsKey("source"))) { if (!(root.containsKey("enabled") && root.containsKey("source"))) {
retMsg[F("message")] = F("Values are missing!"); retMsg["message"] = "Values are missing!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
if (root[F("source")].as<uint8_t>() == PowerMeter.SOURCE_HTTP) { if (root["source"].as<uint8_t>() == PowerMeter.SOURCE_HTTP) {
JsonArray http_phases = root[F("http_phases")]; JsonArray http_phases = root["http_phases"];
for (uint8_t i = 0; i < http_phases.size(); i++) { for (uint8_t i = 0; i < http_phases.size(); i++) {
JsonObject phase = http_phases[i].as<JsonObject>(); JsonObject phase = http_phases[i].as<JsonObject>();
if (i > 0 && !phase[F("enabled")].as<bool>()) { if (i > 0 && !phase["enabled"].as<bool>()) {
continue; continue;
} }
if (i == 0 || phase[F("http_individual_requests")].as<bool>()) { if (i == 0 || phase["http_individual_requests"].as<bool>()) {
if (!phase.containsKey("url") if (!phase.containsKey("url")
|| (!phase[F("url")].as<String>().startsWith("http://") || (!phase["url"].as<String>().startsWith("http://")
&& !phase[F("url")].as<String>().startsWith("https://"))) { && !phase["url"].as<String>().startsWith("https://"))) {
retMsg[F("message")] = F("URL must either start with http:// or https://!"); retMsg["message"] = "URL must either start with http:// or https://!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
if ((phase[F("auth_type")].as<Auth>() != Auth::none) if ((phase["auth_type"].as<Auth>() != Auth::none)
&& ( phase[F("username")].as<String>().length() == 0 || phase[F("password")].as<String>().length() == 0)) { && ( phase["username"].as<String>().length() == 0 || phase["password"].as<String>().length() == 0)) {
retMsg[F("message")] = F("Username or password must not be empty!"); retMsg["message"] = "Username or password must not be empty!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
if (!phase.containsKey("timeout") if (!phase.containsKey("timeout")
|| phase[F("timeout")].as<uint16_t>() <= 0) { || phase["timeout"].as<uint16_t>() <= 0) {
retMsg[F("message")] = F("Timeout must be greater than 0 ms!"); retMsg["message"] = "Timeout must be greater than 0 ms!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -159,8 +159,8 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
} }
if (!phase.containsKey("json_path") if (!phase.containsKey("json_path")
|| phase[F("json_path")].as<String>().length() == 0) { || phase["json_path"].as<String>().length() == 0) {
retMsg[F("message")] = F("Json path must not be empty!"); retMsg["message"] = "Json path must not be empty!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -169,36 +169,33 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.PowerMeter.Enabled = root[F("enabled")].as<bool>(); config.PowerMeter.Enabled = root["enabled"].as<bool>();
config.PowerMeter.VerboseLogging = root[F("verbose_logging")].as<bool>(); config.PowerMeter.VerboseLogging = root["verbose_logging"].as<bool>();
config.PowerMeter.Source = root[F("source")].as<uint8_t>(); config.PowerMeter.Source = root["source"].as<uint8_t>();
config.PowerMeter.Interval = root[F("interval")].as<uint32_t>(); config.PowerMeter.Interval = root["interval"].as<uint32_t>();
strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1)); strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root["mqtt_topic_powermeter_1"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1));
strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2)); strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root["mqtt_topic_powermeter_2"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2));
strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3)); strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root["mqtt_topic_powermeter_3"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3));
config.PowerMeter.SdmBaudrate = root[F("sdmbaudrate")].as<uint32_t>(); config.PowerMeter.SdmBaudrate = root["sdmbaudrate"].as<uint32_t>();
config.PowerMeter.SdmAddress = root[F("sdmaddress")].as<uint8_t>(); config.PowerMeter.SdmAddress = root["sdmaddress"].as<uint8_t>();
config.PowerMeter.HttpIndividualRequests = root[F("http_individual_requests")].as<bool>(); config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as<bool>();
JsonArray http_phases = root[F("http_phases")]; JsonArray http_phases = root["http_phases"];
for (uint8_t i = 0; i < http_phases.size(); i++) { for (uint8_t i = 0; i < http_phases.size(); i++) {
JsonObject phase = http_phases[i].as<JsonObject>(); JsonObject phase = http_phases[i].as<JsonObject>();
config.PowerMeter.Http_Phase[i].Enabled = (i == 0 ? true : phase[F("enabled")].as<bool>()); config.PowerMeter.Http_Phase[i].Enabled = (i == 0 ? true : phase["enabled"].as<bool>());
strlcpy(config.PowerMeter.Http_Phase[i].Url, phase[F("url")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Url)); strlcpy(config.PowerMeter.Http_Phase[i].Url, phase["url"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Url));
config.PowerMeter.Http_Phase[i].AuthType = phase[F("auth_type")].as<Auth>(); config.PowerMeter.Http_Phase[i].AuthType = phase["auth_type"].as<Auth>();
strlcpy(config.PowerMeter.Http_Phase[i].Username, phase[F("username")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Username)); strlcpy(config.PowerMeter.Http_Phase[i].Username, phase["username"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Username));
strlcpy(config.PowerMeter.Http_Phase[i].Password, phase[F("password")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Password)); strlcpy(config.PowerMeter.Http_Phase[i].Password, phase["password"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Password));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, phase[F("header_key")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, phase["header_key"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderKey));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, phase[F("header_value")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, phase["header_value"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderValue));
config.PowerMeter.Http_Phase[i].Timeout = phase[F("timeout")].as<uint16_t>(); config.PowerMeter.Http_Phase[i].Timeout = phase["timeout"].as<uint16_t>();
strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, phase[F("json_path")].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, phase["json_path"].as<String>().c_str(), sizeof(config.PowerMeter.Http_Phase[i].JsonPath));
} }
Configuration.write(); WebApi.writeConfig(retMsg);
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -216,11 +213,11 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse(); AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse();
JsonObject retMsg = asyncJsonResponse->getRoot(); auto& retMsg = asyncJsonResponse->getRoot();
retMsg[F("type")] = F("warning"); retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!"); retMsg["message"] = "No values found!";
asyncJsonResponse->setLength(); asyncJsonResponse->setLength();
request->send(asyncJsonResponse); request->send(asyncJsonResponse);
return; return;
@ -229,7 +226,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
String json = request->getParam("data", true)->value(); String json = request->getParam("data", true)->value();
if (json.length() > 2048) { if (json.length() > 2048) {
retMsg[F("message")] = F("Data too large!"); retMsg["message"] = "Data too large!";
asyncJsonResponse->setLength(); asyncJsonResponse->setLength();
request->send(asyncJsonResponse); request->send(asyncJsonResponse);
return; return;
@ -239,7 +236,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
DeserializationError error = deserializeJson(root, json); DeserializationError error = deserializeJson(root, json);
if (error) { if (error) {
retMsg[F("message")] = F("Failed to parse data!"); retMsg["message"] = "Failed to parse data!";
asyncJsonResponse->setLength(); asyncJsonResponse->setLength();
request->send(asyncJsonResponse); request->send(asyncJsonResponse);
return; return;
@ -248,7 +245,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password") if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password")
|| !root.containsKey("header_key") || !root.containsKey("header_value") || !root.containsKey("header_key") || !root.containsKey("header_value")
|| !root.containsKey("timeout") || !root.containsKey("json_path")) { || !root.containsKey("timeout") || !root.containsKey("json_path")) {
retMsg[F("message")] = F("Missing fields!"); retMsg["message"] = "Missing fields!";
asyncJsonResponse->setLength(); asyncJsonResponse->setLength();
request->send(asyncJsonResponse); request->send(asyncJsonResponse);
return; return;
@ -258,15 +255,15 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
errorMessage[256]; errorMessage[256];
char response[200]; char response[200];
if (HttpPowerMeter.httpRequest(root[F("url")].as<String>().c_str(), if (HttpPowerMeter.httpRequest(root["url"].as<String>().c_str(),
root[F("auth_type")].as<Auth>(), root[F("username")].as<String>().c_str(), root[F("password")].as<String>().c_str(), root["auth_type"].as<Auth>(), root["username"].as<String>().c_str(), root["password"].as<String>().c_str(),
root[F("header_key")].as<String>().c_str(), root[F("header_value")].as<String>().c_str(), root[F("timeout")].as<uint16_t>(), root["header_key"].as<String>().c_str(), root["header_value"].as<String>().c_str(), root["timeout"].as<uint16_t>(),
powerMeterResponse, sizeof(powerMeterResponse), errorMessage, sizeof(errorMessage))) { powerMeterResponse, sizeof(powerMeterResponse), errorMessage, sizeof(errorMessage))) {
float power; float power;
if (HttpPowerMeter.getFloatValueByJsonPath(powerMeterResponse, if (HttpPowerMeter.getFloatValueByJsonPath(powerMeterResponse,
root[F("json_path")].as<String>().c_str(), power)) { root["json_path"].as<String>().c_str(), power)) {
retMsg[F("type")] = F("success"); retMsg["type"] = "success";
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", power); snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", power);
} else { } else {
snprintf_P(response, sizeof(response), "Error: Could not find value for JSON path!"); snprintf_P(response, sizeof(response), "Error: Could not find value for JSON path!");
@ -275,7 +272,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
snprintf_P(response, sizeof(response), errorMessage); snprintf_P(response, sizeof(response), errorMessage);
} }
retMsg[F("message")] = F(response); retMsg["message"] = response;
asyncJsonResponse->setLength(); asyncJsonResponse->setLength();
request->send(asyncJsonResponse); request->send(asyncJsonResponse);
} }

View File

@ -54,6 +54,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# TYPE opendtu_free_heap_size gauge\n"); stream->print("# TYPE opendtu_free_heap_size gauge\n");
stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap());
stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n");
stream->print("# TYPE opendtu_biggest_heap_block gauge\n");
stream->printf("opendtu_biggest_heap_block %zu\n", ESP.getMaxAllocHeap());
stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n");
stream->print("# TYPE opendtu_heap_min_free gauge\n");
stream->printf("opendtu_heap_min_free %zu\n", ESP.getMinFreeHeap());
stream->print("# HELP wifi_rssi WiFi RSSI\n"); stream->print("# HELP wifi_rssi WiFi RSSI\n");
stream->print("# TYPE wifi_rssi gauge\n"); stream->print("# TYPE wifi_rssi gauge\n");
stream->printf("wifi_rssi %d\n", WiFi.RSSI()); stream->printf("wifi_rssi %d\n", WiFi.RSSI());

View File

@ -31,7 +31,7 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root["password"] = config.Security.Password; root["password"] = config.Security.Password;
@ -48,7 +48,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "warning"; retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
@ -101,11 +101,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
strlcpy(config.Security.Password, root["password"].as<String>().c_str(), sizeof(config.Security.Password)); strlcpy(config.Security.Password, root["password"].as<String>().c_str(), sizeof(config.Security.Password));
config.Security.AllowReadonly = root["allow_readonly"].as<bool>(); config.Security.AllowReadonly = root["allow_readonly"].as<bool>();
Configuration.write();
retMsg["type"] = "success"; WebApi.writeConfig(retMsg);
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -118,7 +115,7 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg["type"] = "success"; retMsg["type"] = "success";
retMsg["message"] = "Authentication successful!"; retMsg["message"] = "Authentication successful!";
retMsg["code"] = WebApiError::SecurityAuthSuccess; retMsg["code"] = WebApiError::SecurityAuthSuccess;

View File

@ -40,7 +40,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
root["hostname"] = NetworkSettings.getHostname(); root["hostname"] = NetworkSettings.getHostname();
@ -49,6 +49,8 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root["heap_total"] = ESP.getHeapSize(); root["heap_total"] = ESP.getHeapSize();
root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap(); root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap();
root["heap_max_block"] = ESP.getMaxAllocHeap();
root["heap_min_free"] = ESP.getMinFreeHeap();
root["sketch_total"] = ESP.getFreeSketchSpace(); root["sketch_total"] = ESP.getFreeSketchSpace();
root["sketch_used"] = ESP.getSketchSize(); root["sketch_used"] = ESP.getSketchSize();
root["littlefs_total"] = LittleFS.totalBytes(); root["littlefs_total"] = LittleFS.totalBytes();

View File

@ -33,12 +33,12 @@ void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("vedirect_enabled")] = config.Vedirect.Enabled; root["vedirect_enabled"] = config.Vedirect.Enabled;
root[F("verbose_logging")] = config.Vedirect.VerboseLogging; root["verbose_logging"] = config.Vedirect.VerboseLogging;
root[F("vedirect_updatesonly")] = config.Vedirect.UpdatesOnly; root["vedirect_updatesonly"] = config.Vedirect.UpdatesOnly;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -51,12 +51,12 @@ void WebApiVedirectClass::onVedirectAdminGet(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("vedirect_enabled")] = config.Vedirect.Enabled; root["vedirect_enabled"] = config.Vedirect.Enabled;
root[F("verbose_logging")] = config.Vedirect.VerboseLogging; root["verbose_logging"] = config.Vedirect.VerboseLogging;
root[F("vedirect_updatesonly")] = config.Vedirect.UpdatesOnly; root["vedirect_updatesonly"] = config.Vedirect.UpdatesOnly;
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -69,12 +69,12 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); auto& retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg["type"] = "warning";
if (!request->hasParam("data", true)) { if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!"); retMsg["message"] = "No values found!";
retMsg[F("code")] = WebApiError::GenericNoValueFound; retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -83,8 +83,8 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
String json = request->getParam("data", true)->value(); String json = request->getParam("data", true)->value();
if (json.length() > 1024) { if (json.length() > 1024) {
retMsg[F("message")] = F("Data too large!"); retMsg["message"] = "Data too large!";
retMsg[F("code")] = WebApiError::GenericDataTooLarge; retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -94,8 +94,8 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
DeserializationError error = deserializeJson(root, json); DeserializationError error = deserializeJson(root, json);
if (error) { if (error) {
retMsg[F("message")] = F("Failed to parse data!"); retMsg["message"] = "Failed to parse data!";
retMsg[F("code")] = WebApiError::GenericParseError; retMsg["code"] = WebApiError::GenericParseError;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
@ -104,25 +104,22 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
if (!root.containsKey("vedirect_enabled") || if (!root.containsKey("vedirect_enabled") ||
!root.containsKey("verbose_logging") || !root.containsKey("verbose_logging") ||
!root.containsKey("vedirect_updatesonly") ) { !root.containsKey("vedirect_updatesonly") ) {
retMsg[F("message")] = F("Values are missing!"); retMsg["message"] = "Values are missing!";
retMsg[F("code")] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return;
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.Vedirect.Enabled = root[F("vedirect_enabled")].as<bool>(); config.Vedirect.Enabled = root["vedirect_enabled"].as<bool>();
config.Vedirect.VerboseLogging = root[F("verbose_logging")].as<bool>(); config.Vedirect.VerboseLogging = root["verbose_logging"].as<bool>();
config.Vedirect.UpdatesOnly = root[F("vedirect_updatesonly")].as<bool>(); config.Vedirect.UpdatesOnly = root["vedirect_updatesonly"].as<bool>();
Configuration.write();
VictronMppt.updateSettings(); WebApi.writeConfig(retMsg);
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
retMsg[F("code")] = WebApiError::GenericSuccess;
response->setLength(); response->setLength();
request->send(response); request->send(response);
VictronMppt.updateSettings();
} }

View File

@ -7,6 +7,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "Huawei_can.h" #include "Huawei_can.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "defaults.h" #include "defaults.h"
@ -50,16 +51,15 @@ void WebApiWsHuaweiLiveClass::loop()
_lastUpdateCheck = millis(); _lastUpdateCheck = millis();
try { try {
String buffer; std::lock_guard<std::mutex> lock(_mutex);
// free JsonDocument as soon as possible
{
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
JsonVariant var = root; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);
serializeJson(root, buffer);
}
if (buffer) { String buffer;
serializeJson(root, buffer);
if (Configuration.get().Security.AllowReadonly) { if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", ""); _ws.setAuthentication("", "");
} else { } else {
@ -69,7 +69,9 @@ void WebApiWsHuaweiLiveClass::loop()
_ws.textAll(buffer); _ws.textAll(buffer);
} }
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
} }
} }
@ -78,26 +80,26 @@ void WebApiWsHuaweiLiveClass::generateJsonResponse(JsonVariant& root)
const RectifierParameters_t * rp = HuaweiCan.get(); const RectifierParameters_t * rp = HuaweiCan.get();
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
root[F("input_voltage")]["v"] = rp->input_voltage; root["input_voltage"]["v"] = rp->input_voltage;
root[F("input_voltage")]["u"] = "V"; root["input_voltage"]["u"] = "V";
root[F("input_current")]["v"] = rp->input_current; root["input_current"]["v"] = rp->input_current;
root[F("input_current")]["u"] = "A"; root["input_current"]["u"] = "A";
root[F("input_power")]["v"] = rp->input_power; root["input_power"]["v"] = rp->input_power;
root[F("input_power")]["u"] = "W"; root["input_power"]["u"] = "W";
root[F("output_voltage")]["v"] = rp->output_voltage; root["output_voltage"]["v"] = rp->output_voltage;
root[F("output_voltage")]["u"] = "V"; root["output_voltage"]["u"] = "V";
root[F("output_current")]["v"] = rp->output_current; root["output_current"]["v"] = rp->output_current;
root[F("output_current")]["u"] = "A"; root["output_current"]["u"] = "A";
root[F("max_output_current")]["v"] = rp->max_output_current; root["max_output_current"]["v"] = rp->max_output_current;
root[F("max_output_current")]["u"] = "A"; root["max_output_current"]["u"] = "A";
root[F("output_power")]["v"] = rp->output_power; root["output_power"]["v"] = rp->output_power;
root[F("output_power")]["u"] = "W"; root["output_power"]["u"] = "W";
root[F("input_temp")]["v"] = rp->input_temp; root["input_temp"]["v"] = rp->input_temp;
root[F("input_temp")]["u"] = "°C"; root["input_temp"]["u"] = "°C";
root[F("output_temp")]["v"] = rp->output_temp; root["output_temp"]["v"] = rp->output_temp;
root[F("output_temp")]["u"] = "°C"; root["output_temp"]["u"] = "°C";
root[F("efficiency")]["v"] = rp->efficiency * 100; root["efficiency"]["v"] = rp->efficiency * 100;
root[F("efficiency")]["u"] = "%"; root["efficiency"]["u"] = "%";
} }
@ -122,15 +124,19 @@ void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
return; return;
} }
try { try {
std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U); AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
JsonVariant root = response->getRoot().as<JsonVariant>(); auto& root = response->getRoot();
generateJsonResponse(root); generateJsonResponse(root);
response->setLength(); response->setLength();
request->send(response); request->send(response);
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
WebApi.sendTooManyRequests(request);
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what());
WebApi.sendTooManyRequests(request); WebApi.sendTooManyRequests(request);
} }
} }

View File

@ -9,6 +9,7 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "WebApi.h" #include "WebApi.h"
#include "defaults.h" #include "defaults.h"
#include "Utils.h"
WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass() WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass()
: _ws("/batterylivedata") : _ws("/batterylivedata")
@ -48,16 +49,15 @@ void WebApiWsBatteryLiveClass::loop()
_lastUpdateCheck = millis(); _lastUpdateCheck = millis();
try { try {
String buffer; std::lock_guard<std::mutex> lock(_mutex);
// free JsonDocument as soon as possible
{
DynamicJsonDocument root(_responseSize); DynamicJsonDocument root(_responseSize);
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
JsonVariant var = root; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);
serializeJson(root, buffer);
}
if (buffer) { String buffer;
serializeJson(root, buffer);
if (Configuration.get().Security.AllowReadonly) { if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", ""); _ws.setAuthentication("", "");
} else { } else {
@ -68,6 +68,8 @@ void WebApiWsBatteryLiveClass::loop()
} }
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
} }
} }
@ -91,15 +93,18 @@ void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
return; return;
} }
try { try {
std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize); AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize);
JsonVariant root = response->getRoot().as<JsonVariant>(); auto& root = response->getRoot();
generateJsonResponse(root); generateJsonResponse(root);
response->setLength(); response->setLength();
request->send(response); request->send(response);
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
WebApi.sendTooManyRequests(request);
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what());
WebApi.sendTooManyRequests(request); WebApi.sendTooManyRequests(request);
} }
} }

View File

@ -6,6 +6,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "Datastore.h" #include "Datastore.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "Battery.h" #include "Battery.h"
#include "Huawei_can.h" #include "Huawei_can.h"
@ -67,20 +68,12 @@ void WebApiWsLiveClass::loop()
try { try {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
DynamicJsonDocument root(4200 * INV_MAX_COUNT); // TODO(helge) check if this calculation is correct DynamicJsonDocument root(4200 * INV_MAX_COUNT);
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
// TODO(helge) temporary dump of memory usage if allocation of DynamicJsonDocument fails (will be fixed in upstream repo)
if (root.capacity() == 0) {
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: Alloc memory for DynamicJsonDocument failed (FreeHeap = %d, MaxAllocHeap = %d, MinFreeHeap = %d).\r\n", ESP.getFreeHeap(), ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
_lastWsPublish = millis();
return;
}
JsonVariant var = root; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);
String buffer; String buffer;
if (buffer) {
serializeJson(root, buffer); serializeJson(root, buffer);
if (Configuration.get().Security.AllowReadonly) { if (Configuration.get().Security.AllowReadonly) {
@ -191,7 +184,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
} }
JsonObject vedirectObj = root.createNestedObject("vedirect"); JsonObject vedirectObj = root.createNestedObject("vedirect");
vedirectObj[F("enabled")] = Configuration.get().Vedirect.Enabled; vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled;
JsonObject totalVeObj = vedirectObj.createNestedObject("total"); JsonObject totalVeObj = vedirectObj.createNestedObject("total");
addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1);
@ -199,16 +192,16 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2);
JsonObject huaweiObj = root.createNestedObject("huawei"); JsonObject huaweiObj = root.createNestedObject("huawei");
huaweiObj[F("enabled")] = Configuration.get().Huawei.Enabled; huaweiObj["enabled"] = Configuration.get().Huawei.Enabled;
const RectifierParameters_t * rp = HuaweiCan.get(); const RectifierParameters_t * rp = HuaweiCan.get();
addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); addTotalField(huaweiObj, "Power", rp->output_power, "W", 2);
JsonObject batteryObj = root.createNestedObject("battery"); JsonObject batteryObj = root.createNestedObject("battery");
batteryObj[F("enabled")] = Configuration.get().Battery.Enabled; batteryObj["enabled"] = Configuration.get().Battery.Enabled;
addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0); addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0);
JsonObject powerMeterObj = root.createNestedObject("power_meter"); JsonObject powerMeterObj = root.createNestedObject("power_meter");
powerMeterObj[F("enabled")] = Configuration.get().PowerMeter.Enabled; powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled;
addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1);
} }
@ -255,7 +248,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
try { try {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4200 * INV_MAX_COUNT); AsyncJsonResponse* response = new AsyncJsonResponse(false, 4200 * INV_MAX_COUNT);
JsonVariant root = response->getRoot(); auto& root = response->getRoot();
generateJsonResponse(root); generateJsonResponse(root);

View File

@ -6,6 +6,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "defaults.h" #include "defaults.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
@ -55,16 +56,15 @@ void WebApiWsVedirectLiveClass::loop()
if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) {
try { try {
String buffer; std::lock_guard<std::mutex> lock(_mutex);
// free JsonDocument as soon as possible
{
DynamicJsonDocument root(_responseSize); DynamicJsonDocument root(_responseSize);
if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
JsonVariant var = root; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);
serializeJson(root, buffer);
}
if (buffer) { String buffer;
serializeJson(root, buffer);
if (Configuration.get().Security.AllowReadonly) { if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", ""); _ws.setAuthentication("", "");
} else { } else {
@ -76,6 +76,8 @@ void WebApiWsVedirectLiveClass::loop()
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what());
} }
_lastWsPublish = millis(); _lastWsPublish = millis();
@ -168,8 +170,9 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
return; return;
} }
try { try {
std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize); AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize);
JsonVariant root = response->getRoot(); auto& root = response->getRoot();
generateJsonResponse(root); generateJsonResponse(root);
@ -177,8 +180,10 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
request->send(response); request->send(response);
} catch (std::bad_alloc& bad_alloc) { } catch (std::bad_alloc& bad_alloc) {
MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
WebApi.sendTooManyRequests(request);
} catch (const std::exception& exc) {
MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what());
WebApi.sendTooManyRequests(request); WebApi.sendTooManyRequests(request);
} }
} }

View File

@ -18,7 +18,7 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"sortablejs": "^1.15.1", "sortablejs": "^1.15.1",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.4.3", "vue": "^3.4.5",
"vue-i18n": "^9.8.0", "vue-i18n": "^9.8.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
@ -36,7 +36,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2", "eslint-plugin-vue": "^9.19.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.69.6", "sass": "^1.69.7",
"terser": "^5.26.0", "terser": "^5.26.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.10", "vite": "^5.0.10",

View File

@ -0,0 +1,55 @@
<template>
<CardElement :text="$t('heapdetails.HeapDetails')" textVariant="text-bg-primary">
<div class="table-responsive">
<table class="table table-hover table-condensed">
<tbody>
<tr>
<th>{{ $t('heapdetails.TotalFree') }}</th>
<td>{{ $n(Math.round(getFreeHeap() / 1024), 'kilobyte') }}</td>
</tr>
<tr>
<th>{{ $t('heapdetails.LargestFreeBlock') }}</th>
<td>{{ $n(Math.round(systemStatus.heap_max_block / 1024), 'kilobyte') }}</td>
</tr>
<tr>
<th>{{ $t('heapdetails.Fragmentation') }}</th>
<td>{{ $n(getFragmentation(), 'percent') }}</td>
</tr>
<tr>
<th>{{ $t('heapdetails.MaxUsage') }}</th>
<td>{{ $n(Math.round(getMaxUsageAbs() / 1024), 'kilobyte') }} ({{ $n(getMaxUsageRel(), 'percent') }})</td>
</tr>
</tbody>
</table>
</div>
</CardElement>
</template>
<script lang="ts">
import CardElement from '@/components/CardElement.vue';
import type { SystemStatus } from '@/types/SystemStatus';
import { defineComponent, type PropType } from 'vue';
export default defineComponent({
components: {
CardElement,
},
props: {
systemStatus: { type: Object as PropType<SystemStatus>, required: true },
},
methods: {
getFreeHeap() {
return this.systemStatus.heap_total - this.systemStatus.heap_used;
},
getMaxUsageAbs() {
return this.systemStatus.heap_total - this.systemStatus.heap_min_free;
},
getMaxUsageRel() {
return this.getMaxUsageAbs() / this.systemStatus.heap_total;
},
getFragmentation() {
return 1 - (this.systemStatus.heap_max_block / this.getFreeHeap());
},
},
});
</script>

View File

@ -48,6 +48,7 @@
"1003": "Daten zu groß!", "1003": "Daten zu groß!",
"1004": "Fehler beim interpretieren der Daten!", "1004": "Fehler beim interpretieren der Daten!",
"1005": "Benötigte Werte fehlen!", "1005": "Benötigte Werte fehlen!",
"1006": "Schreiben fehlgeschlagen!",
"2001": "Die Seriennummer darf nicht 0 sein!", "2001": "Die Seriennummer darf nicht 0 sein!",
"2002": "Das Abfraginterval muss größer als 0 sein!", "2002": "Das Abfraginterval muss größer als 0 sein!",
"2003": "Ungültige Sendeleistung angegeben!", "2003": "Ungültige Sendeleistung angegeben!",
@ -248,6 +249,13 @@
"LittleFs": "LittleFs", "LittleFs": "LittleFs",
"Sketch": "Sketch" "Sketch": "Sketch"
}, },
"heapdetails": {
"HeapDetails": "Detailinformationen zum Heap",
"TotalFree": "Insgesamt frei",
"LargestFreeBlock": "Größter zusammenhängender freier Block",
"MaxUsage": "Maximale Speichernutzung seit Start",
"Fragmentation": "Grad der Fragmentierung"
},
"radioinfo": { "radioinfo": {
"RadioInformation": "Funkmodulinformationen", "RadioInformation": "Funkmodulinformationen",
"Status": "{module} Status", "Status": "{module} Status",

View File

@ -48,6 +48,7 @@
"1003": "Data too large!", "1003": "Data too large!",
"1004": "Failed to parse data!", "1004": "Failed to parse data!",
"1005": "Values are missing!", "1005": "Values are missing!",
"1006": "Write failed!",
"2001": "Serial cannot be zero!", "2001": "Serial cannot be zero!",
"2002": "Poll interval must be greater zero!", "2002": "Poll interval must be greater zero!",
"2003": "Invalid power level setting!", "2003": "Invalid power level setting!",
@ -249,6 +250,13 @@
"LittleFs": "LittleFs", "LittleFs": "LittleFs",
"Sketch": "Sketch" "Sketch": "Sketch"
}, },
"heapdetails": {
"HeapDetails": "Heap Details",
"TotalFree": "Total free",
"LargestFreeBlock": "Biggest contiguous free block",
"MaxUsage": "Maximum usage since start",
"Fragmentation": "Level of fragmentation"
},
"radioinfo": { "radioinfo": {
"RadioInformation": "Radio Information", "RadioInformation": "Radio Information",
"Status": "{module} Status", "Status": "{module} Status",

View File

@ -48,6 +48,7 @@
"1003": "Données trop importantes !", "1003": "Données trop importantes !",
"1004": "Échec de l'analyse des données !", "1004": "Échec de l'analyse des données !",
"1005": "Certaines valeurs sont manquantes !", "1005": "Certaines valeurs sont manquantes !",
"1006": "Write failed!",
"2001": "Le numéro de série ne peut pas être nul !", "2001": "Le numéro de série ne peut pas être nul !",
"2002": "L'intervalle de sondage doit être supérieur à zéro !", "2002": "L'intervalle de sondage doit être supérieur à zéro !",
"2003": "Réglage du niveau de puissance invalide !", "2003": "Réglage du niveau de puissance invalide !",
@ -248,6 +249,13 @@
"LittleFs": "LittleFs", "LittleFs": "LittleFs",
"Sketch": "Sketch" "Sketch": "Sketch"
}, },
"heapdetails": {
"HeapDetails": "Heap Details",
"TotalFree": "Total free",
"LargestFreeBlock": "Biggest contiguous free block",
"MaxUsage": "Maximum usage since start",
"Fragmentation": "Level of fragmentation"
},
"radioinfo": { "radioinfo": {
"RadioInformation": "Informations sur la radio", "RadioInformation": "Informations sur la radio",
"Status": "{module} Statut", "Status": "{module} Statut",

View File

@ -22,6 +22,8 @@ export interface SystemStatus {
// MemoryInfo // MemoryInfo
heap_total: number; heap_total: number;
heap_used: number; heap_used: number;
heap_max_block: number;
heap_min_free: number;
littlefs_total: number; littlefs_total: number;
littlefs_used: number; littlefs_used: number;
sketch_total: number; sketch_total: number;

View File

@ -6,6 +6,8 @@
<div class="mt-5"></div> <div class="mt-5"></div>
<MemoryInfo :systemStatus="systemDataList" /> <MemoryInfo :systemStatus="systemDataList" />
<div class="mt-5"></div> <div class="mt-5"></div>
<HeapDetails :systemStatus="systemDataList" />
<div class="mt-5"></div>
<RadioInfo :systemStatus="systemDataList" /> <RadioInfo :systemStatus="systemDataList" />
<div class="mt-5"></div> <div class="mt-5"></div>
</BasePage> </BasePage>
@ -16,6 +18,7 @@ import BasePage from '@/components/BasePage.vue';
import FirmwareInfo from "@/components/FirmwareInfo.vue"; import FirmwareInfo from "@/components/FirmwareInfo.vue";
import HardwareInfo from "@/components/HardwareInfo.vue"; import HardwareInfo from "@/components/HardwareInfo.vue";
import MemoryInfo from "@/components/MemoryInfo.vue"; import MemoryInfo from "@/components/MemoryInfo.vue";
import HeapDetails from "@/components/HeapDetails.vue";
import RadioInfo from "@/components/RadioInfo.vue"; import RadioInfo from "@/components/RadioInfo.vue";
import type { SystemStatus } from '@/types/SystemStatus'; import type { SystemStatus } from '@/types/SystemStatus';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
@ -27,6 +30,7 @@ export default defineComponent({
FirmwareInfo, FirmwareInfo,
HardwareInfo, HardwareInfo,
MemoryInfo, MemoryInfo,
HeapDetails,
RadioInfo, RadioInfo,
}, },
data() { data() {

View File

@ -594,13 +594,13 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-core@3.4.3": "@vue/compiler-core@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.3.tgz#8e8f88273f061cf0a49bf958255f5f0621f12d8b" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.5.tgz#9565aebaadef8649eb7c8e150a5f4f4e2542667d"
integrity sha512-u8jzgFg0EDtSrb/hG53Wwh1bAOQFtc1ZCegBpA/glyvTlgHl+tq13o1zvRfLbegYUw/E4mSTGOiCnAJ9SJ+lsg== integrity sha512-Daka7P1z2AgKjzuueWXhwzIsKu0NkLB6vGbNVEV2iJ8GJTrzraZo/Sk4GWCMRtd/qVi3zwnk+Owbd/xSZbwHtQ==
dependencies: dependencies:
"@babel/parser" "^7.23.6" "@babel/parser" "^7.23.6"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
entities "^4.5.0" entities "^4.5.0"
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map-js "^1.0.2" source-map-js "^1.0.2"
@ -613,13 +613,13 @@
"@vue/compiler-core" "3.2.47" "@vue/compiler-core" "3.2.47"
"@vue/shared" "3.2.47" "@vue/shared" "3.2.47"
"@vue/compiler-dom@3.4.3": "@vue/compiler-dom@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.3.tgz#bea8acde9585d5ce92a3f11c062c863fb33e44d7" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.5.tgz#c53c9d7715b777b1d6d2adcbc491bfd4f9510edd"
integrity sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg== integrity sha512-J8YlxknJVd90SXFJ4HwGANSAXsx5I0lK30sO/zvYV7s5gXf7gZR7r/1BmZ2ju7RGH1lnc6bpBc6nL61yW+PsAQ==
dependencies: dependencies:
"@vue/compiler-core" "3.4.3" "@vue/compiler-core" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
"@vue/compiler-dom@^3.3.0": "@vue/compiler-dom@^3.3.0":
version "3.3.2" version "3.3.2"
@ -629,16 +629,16 @@
"@vue/compiler-core" "3.3.2" "@vue/compiler-core" "3.3.2"
"@vue/shared" "3.3.2" "@vue/shared" "3.3.2"
"@vue/compiler-sfc@3.4.3": "@vue/compiler-sfc@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.3.tgz#a9d35b2deef38576dedd9938851c032fb2ca8617" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.5.tgz#f93f986dfc5c7f72b9a5e00b48be75d9116cc948"
integrity sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw== integrity sha512-jauvkDuSSUbP0ebhfNqljhShA90YEfX/0wZ+w40oZF43IjGyWYjqYaJbvMJwGOd+9+vODW6eSvnk28f0SGV7OQ==
dependencies: dependencies:
"@babel/parser" "^7.23.6" "@babel/parser" "^7.23.6"
"@vue/compiler-core" "3.4.3" "@vue/compiler-core" "3.4.5"
"@vue/compiler-dom" "3.4.3" "@vue/compiler-dom" "3.4.5"
"@vue/compiler-ssr" "3.4.3" "@vue/compiler-ssr" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.30.5" magic-string "^0.30.5"
postcss "^8.4.32" postcss "^8.4.32"
@ -668,13 +668,13 @@
"@vue/compiler-dom" "3.2.47" "@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47" "@vue/shared" "3.2.47"
"@vue/compiler-ssr@3.4.3": "@vue/compiler-ssr@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.3.tgz#c3f641a15a04893b5bc3278f3dac65bed44dce1d" resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.5.tgz#d412a4c9b10d69172a5ce0ec78de98dad441a58d"
integrity sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA== integrity sha512-DDdEcDzj2lWTMfUMMtEpLDhURai9LhM0zSZ219jCt7b2Vyl0/jy3keFgCPMitG0V1S1YG4Cmws3lWHWdxHQOpg==
dependencies: dependencies:
"@vue/compiler-dom" "3.4.3" "@vue/compiler-dom" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
"@vue/devtools-api@^6.5.0": "@vue/devtools-api@^6.5.0":
version "6.5.0" version "6.5.0"
@ -716,37 +716,37 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.25.7" magic-string "^0.25.7"
"@vue/reactivity@3.4.3": "@vue/reactivity@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.3.tgz#95287b5950b328df4a942a7cf14a0e13487f1eac" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.5.tgz#68bc91cd356eed95dc5e9e0570e3f7becaee578b"
integrity sha512-q5f9HLDU+5aBKizXHAx0w4whkIANs1Muiq9R5YXm0HtorSlflqv9u/ohaMxuuhHWCji4xqpQ1eL04WvmAmGnFg== integrity sha512-BcWkKvjdvqJwb7BhhFkXPLDCecX4d4a6GATvCduJQDLv21PkPowAE5GKuIE5p6RC07/Lp9FMkkq4AYCTVF5KlQ==
dependencies: dependencies:
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
"@vue/runtime-core@3.4.3": "@vue/runtime-core@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.3.tgz#fe7649a93d9b20b9b351cd699f69f0e34a26e3ab" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.5.tgz#2bf253a6f6b0430af1aacf0fdfd8f5782feefce9"
integrity sha512-C1r6QhB1qY7D591RCSFhMULyzL9CuyrGc+3PpB0h7dU4Qqw6GNyo4BNFjHZVvsWncrUlKX3DIKg0Y7rNNr06NQ== integrity sha512-wh9ELIOQKeWT9SaUPdLrsxRkZv14jp+SJm9aiQGWio+/MWNM3Lib0wE6CoKEqQ9+SCYyGjDBhTOTtO47kCgbkg==
dependencies: dependencies:
"@vue/reactivity" "3.4.3" "@vue/reactivity" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
"@vue/runtime-dom@3.4.3": "@vue/runtime-dom@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.3.tgz#54a6115cfba364f20cdf5a44c2ff87337a57def8" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.5.tgz#b43736d66c32f6038778024587592cb9d68495de"
integrity sha512-wrsprg7An5Ec+EhPngWdPuzkp0BEUxAKaQtN9dPU/iZctPyD9aaXmVtehPJerdQxQale6gEnhpnfywNw3zOv2A== integrity sha512-n5ewvOjyG3IEpqGBahdPXODFSpVlSz3H4LF76Sx0XAqpIOqyJ5bIb2PrdYuH2ogBMAQPh+o5tnoH4nJpBr8U0Q==
dependencies: dependencies:
"@vue/runtime-core" "3.4.3" "@vue/runtime-core" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
csstype "^3.1.3" csstype "^3.1.3"
"@vue/server-renderer@3.4.3": "@vue/server-renderer@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.3.tgz#c508f58b9f83f0959085d5aa6854eac9141b4bc6" resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.5.tgz#4bfa7aa763217d8b2d4767d2c8d968a9d40352c1"
integrity sha512-BUxt8oVGMKKsqSkM1uU3d3Houyfy4WAc2SpSQRebNd+XJGATVkW/rO129jkyL+kpB/2VRKzE63zwf5RtJ3XuZw== integrity sha512-jOFc/VE87yvifQpNju12VcqimH8pBLxdcT+t3xMeiED1K6DfH9SORyhFEoZlW5TG2Vwfn3Ul5KE+1aC99xnSBg==
dependencies: dependencies:
"@vue/compiler-ssr" "3.4.3" "@vue/compiler-ssr" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
"@vue/shared@3.2.47": "@vue/shared@3.2.47":
version "3.2.47" version "3.2.47"
@ -758,10 +758,10 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3"
integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ== integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ==
"@vue/shared@3.4.3": "@vue/shared@3.4.5":
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.3.tgz#01d54b32b9796c85c853c670d9395a813f23a8c2" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.5.tgz#c8b4eb6399a7fc986565ea736d938b3a1579256d"
integrity sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ== integrity sha512-6XptuzlMvN4l4cDnDw36pdGEV+9njYkQ1ZE0Q6iZLwrKefKaOJyiFmcP3/KBDHbt72cJZGtllAc1GaHe6XGAyg==
"@vue/tsconfig@^0.5.1": "@vue/tsconfig@^0.5.1":
version "0.5.1" version "0.5.1"
@ -2238,10 +2238,10 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
is-regex "^1.1.4" is-regex "^1.1.4"
sass@^1.69.6: sass@^1.69.7:
version "1.69.6" version "1.69.7"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.6.tgz#88ae1f93facc46d2da9b0bdd652d65068bcfa397" resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.7.tgz#6e7e1c8f51e8162faec3e9619babc7da780af3b7"
integrity sha512-qbRr3k9JGHWXCvZU77SD2OTwUlC+gNT+61JOLcmLm+XqH4h/5D+p4IIsxvpkB89S9AwJOyb5+rWNpIucaFxSFQ== integrity sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==
dependencies: dependencies:
chokidar ">=3.0.0 <4.0.0" chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0" immutable "^4.0.0"
@ -2608,16 +2608,16 @@ vue-tsc@^1.8.27:
"@vue/language-core" "1.8.27" "@vue/language-core" "1.8.27"
semver "^7.5.4" semver "^7.5.4"
vue@^3.4.3: vue@^3.4.5:
version "3.4.3" version "3.4.5"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.3.tgz#e1ba36a64134dcedc12cfb2c28e7cd15ba121f04" resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.5.tgz#c08b9d903a20faaf4df7270bf2fa7487741b2294"
integrity sha512-GjN+culMAGv/mUbkIv8zMKItno8npcj5gWlXkSxf1SPTQf8eJ4A+YfHIvQFyL1IfuJcMl3soA7SmN1fRxbf/wA== integrity sha512-VH6nHFhLPjgu2oh5vEBXoNZxsGHuZNr3qf4PHClwJWw6IDqw6B3x+4J+ABdoZ0aJuT8Zi0zf3GpGlLQCrGWHrw==
dependencies: dependencies:
"@vue/compiler-dom" "3.4.3" "@vue/compiler-dom" "3.4.5"
"@vue/compiler-sfc" "3.4.3" "@vue/compiler-sfc" "3.4.5"
"@vue/runtime-dom" "3.4.3" "@vue/runtime-dom" "3.4.5"
"@vue/server-renderer" "3.4.3" "@vue/server-renderer" "3.4.5"
"@vue/shared" "3.4.3" "@vue/shared" "3.4.5"
webpack-sources@^3.2.3: webpack-sources@^3.2.3:
version "3.2.3" version "3.2.3"

Binary file not shown.

Binary file not shown.

Binary file not shown.