Merge branch 'tbnobody:master' into master

This commit is contained in:
Rob0xFF 2024-04-30 08:46:42 +02:00 committed by GitHub
commit ab7a30eaf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1306 additions and 1611 deletions

54
.github/workflows/repo-maintenance.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: 'Repository Maintenance'
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock
jobs:
stale:
name: 'Stale'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 14
days-before-close: 60
any-of-labels: 'cant-reproduce,not a bug'
stale-issue-label: stale
stale-pr-label: stale
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
lock-threads:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '30'
pr-inactive-days: '30'
discussion-inactive-days: '30'
log-output: true
issue-comment: >
This issue has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns.
pr-comment: >
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion or issue for related concerns.
discussion-comment: >
This discussion has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new discussion for related concerns.

View File

@ -5,7 +5,7 @@
#include <cstdint>
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011b00 // 0.1.27 // make sure to clean all after change
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64
@ -30,8 +30,6 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define JSON_BUFFER_SIZE 12288
struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower;
char Name[CHAN_MAX_NAME_STRLEN];

View File

@ -66,10 +66,10 @@ private:
void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100);
void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off);
static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(DynamicJsonDocument& doc);
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(JsonDocument& doc);
static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = "");
static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = "");
static String getDtuUniqueId();
static String getDtuUrl();

View File

@ -10,6 +10,6 @@ public:
static uint64_t generateDtuSerial();
static int getTimezoneOffset();
static void restartDtu();
static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles();
};

View File

@ -22,6 +22,7 @@
#include "WebApi_webapp.h"
#include "WebApi_ws_console.h"
#include "WebApi_ws_live.h"
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
@ -37,6 +38,10 @@ public:
static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!");
static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document);
static uint64_t parseSerialFromRequest(AsyncWebServerRequest* request, String param_name = "inv");
static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line);
private:
AsyncWebServer _server;

View File

@ -5,10 +5,11 @@ enum WebApiError {
GenericBase = 1000,
GenericSuccess,
GenericNoValueFound,
GenericDataTooLarge,
GenericDataTooLarge, // not used anymore
GenericParseError,
GenericValueMissing,
GenericWriteFailed,
GenericInternalServerError,
DtuBase = 2000,
DtuSerialZero,

View File

@ -4,8 +4,6 @@
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#define MQTT_JSON_DOC_SIZE 10240
class WebApiMqttClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);

View File

@ -22,7 +22,8 @@
#define MDNS_ENABLED false
#define NTP_SERVER "pool.ntp.org"
#define NTP_SERVER_OLD "pool.ntp.org"
#define NTP_SERVER "opendtu.pool.ntp.org"
#define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
#define NTP_TIMEZONEDESCR "Europe/Berlin"
#define NTP_LONGITUDE 10.4515f

View File

@ -114,7 +114,7 @@ void HoymilesClass::loop()
}
// Fetch grid profile
if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) {
if (iv->Statistics()->getLastUpdate() > 0 && (iv->GridProfile()->getLastUpdate() == 0 || !iv->GridProfile()->containsValidData())) {
iv->sendGridOnProFileParaRequest();
}

View File

@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "TimeoutHelper.h"
#include "commands/CommandAbstract.h"
#include "types.h"
#include <memory>
#include <ThreadSafeQueue.h>
#include <TimeoutHelper.h>
#include <memory>
class HoymilesRadio {
public:
@ -43,4 +43,4 @@ protected:
bool _busyFlag = false;
TimeoutHelper _rxTimeout;
};
};

View File

@ -70,7 +70,7 @@ bool HMT_4CH::isValidSerial(const uint64_t serial)
String HMT_4CH::typeName() const
{
return F("HMT-1600/1800/2000-4T");
return "HMT-1600/1800/2000-4T";
}
const byteAssign_t* HMT_4CH::getByteAssignment() const

View File

@ -84,7 +84,7 @@ bool HMT_6CH::isValidSerial(const uint64_t serial)
String HMT_6CH::typeName() const
{
return F("HMT-1800/2250-6T");
return "HMT-1800/2250-6T";
}
const byteAssign_t* HMT_6CH::getByteAssignment() const

View File

@ -204,7 +204,7 @@ bool DevInfoParser::containsValidData() const
struct tm info;
localtime_r(&t, &info);
return info.tm_year > (2016 - 1900) || getHwPartNumber() == 124097;
return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097;
}
uint8_t DevInfoParser::getDevIdx() const

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Thomas Basler and others
* Copyright (C) 2023 - 2024 Thomas Basler and others
*/
#include "GridProfileParser.h"
#include "../Hoymiles.h"
@ -446,6 +446,11 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
return l;
}
bool GridProfileParser::containsValidData() const
{
return _gridProfileLength > 6;
}
uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version)
{
uint8_t count = 0;

View File

@ -43,6 +43,8 @@ public:
std::list<GridProfileSection_t> getProfile() const;
bool containsValidData() const;
private:
static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version);
static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version);
@ -52,4 +54,4 @@ private:
static const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> _profileTypes;
static const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> _profileValues;
};
};

View File

View File

@ -0,0 +1,13 @@
{
"name": "ThreadSafeQueue",
"keywords": "queue, threadsafe",
"description": "An Arduino for ESP32 thread safe queue implementation",
"authors": {
"name": "Thomas Basler"
},
"version": "0.0.1",
"frameworks": "arduino",
"platforms": [
"espressif32"
]
}

View File

View File

@ -0,0 +1,13 @@
{
"name": "TimeoutHelper",
"keywords": "timeout",
"description": "An Arduino for ESP32 timeout helper",
"authors": {
"name": "Thomas Basler"
},
"version": "0.0.1",
"frameworks": "arduino",
"platforms": [
"espressif32"
]
}

View File

@ -0,0 +1,26 @@
diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
+++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp
@@ -97,7 +97,7 @@
static inline bool _init_async_event_queue(){
if(!_async_queue){
- _async_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *));
+ _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE, sizeof(lwip_event_packet_t *));
if(!_async_queue){
return false;
}
diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
+++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h
@@ -53,6 +53,10 @@
#define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2
#endif
+#ifndef CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE
+#define CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE 32
+#endif
+
class AsyncClient;
#define ASYNC_MAX_ACK_TIME 5000

View File

@ -1,13 +0,0 @@
diff --git a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp
index 12be5f8..8505f73 100644
--- a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp
+++ b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp
@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len)
IPAddress AsyncWebSocketClient::remoteIP() const
{
if (!_client)
- return IPAddress(0U);
+ return IPAddress((uint32_t)0);
return _client->remoteIP();
}

View File

@ -19,12 +19,13 @@ extra_configs =
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
framework = arduino
platform = espressif32@6.5.0
platform = espressif32@6.6.0
build_flags =
-DPIOENV=\"$PIOENV\"
-D_TASK_STD_FUNCTION=1
-D_TASK_THREAD_SAFE=1
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
; Have to remove -Werror because of
; https://github.com/espressif/arduino-esp32/issues/9044 and
@ -36,11 +37,11 @@ build_unflags =
-std=gnu++11
lib_deps =
mathieucarbou/ESP Async WebServer @ 2.8.1
bblanchon/ArduinoJson @ ^6.21.5
mathieucarbou/ESP Async WebServer @ 2.9.3
bblanchon/ArduinoJson @ ^7.0.4
https://github.com/bertmelis/espMqttClient.git#v1.6.0
nrf24/RF24 @ ^1.4.8
olikraus/U8g2 @ ^2.35.14
olikraus/U8g2 @ ^2.35.17
buelowp/sunset @ ^1.1.7
https://github.com/arkhipenko/TaskScheduler#testing
@ -59,7 +60,7 @@ board_build.embed_files =
webapp_dist/js/app.js.gz
webapp_dist/site.webmanifest
custom_patches =
custom_patches = async_tcp
monitor_filters = esp32_exception_decoder, time, log2file, colorize
monitor_speed = 115200
@ -87,13 +88,13 @@ build_flags = ${env.build_flags}
[env:generic_esp32c3]
board = esp32-c3-devkitc-02
custom_patches = ${env.custom_patches},esp32c3
custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags}
[env:generic_esp32c3_usb]
board = esp32-c3-devkitc-02
custom_patches = ${env.custom_patches},esp32c3
custom_patches = ${env.custom_patches}
build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1

View File

@ -25,17 +25,13 @@ bool ConfigurationClass::write()
}
config.Cfg.SaveCount++;
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
JsonDocument doc;
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
JsonObject cfg = doc.createNestedObject("cfg");
JsonObject cfg = doc["cfg"].to<JsonObject>();
cfg["version"] = config.Cfg.Version;
cfg["save_count"] = config.Cfg.SaveCount;
JsonObject wifi = doc.createNestedObject("wifi");
JsonObject wifi = doc["wifi"].to<JsonObject>();
wifi["ssid"] = config.WiFi.Ssid;
wifi["password"] = config.WiFi.Password;
wifi["ip"] = IPAddress(config.WiFi.Ip).toString();
@ -47,10 +43,10 @@ bool ConfigurationClass::write()
wifi["hostname"] = config.WiFi.Hostname;
wifi["aptimeout"] = config.WiFi.ApTimeout;
JsonObject mdns = doc.createNestedObject("mdns");
JsonObject mdns = doc["mdns"].to<JsonObject>();
mdns["enabled"] = config.Mdns.Enabled;
JsonObject ntp = doc.createNestedObject("ntp");
JsonObject ntp = doc["ntp"].to<JsonObject>();
ntp["server"] = config.Ntp.Server;
ntp["timezone"] = config.Ntp.Timezone;
ntp["timezone_descr"] = config.Ntp.TimezoneDescr;
@ -58,7 +54,7 @@ bool ConfigurationClass::write()
ntp["longitude"] = config.Ntp.Longitude;
ntp["sunsettype"] = config.Ntp.SunsetType;
JsonObject mqtt = doc.createNestedObject("mqtt");
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
mqtt["enabled"] = config.Mqtt.Enabled;
mqtt["hostname"] = config.Mqtt.Hostname;
mqtt["port"] = config.Mqtt.Port;
@ -69,27 +65,27 @@ bool ConfigurationClass::write()
mqtt["publish_interval"] = config.Mqtt.PublishInterval;
mqtt["clean_session"] = config.Mqtt.CleanSession;
JsonObject mqtt_lwt = mqtt.createNestedObject("lwt");
JsonObject mqtt_lwt = mqtt["lwt"].to<JsonObject>();
mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic;
mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online;
mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline;
mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos;
JsonObject mqtt_tls = mqtt.createNestedObject("tls");
JsonObject mqtt_tls = mqtt["tls"].to<JsonObject>();
mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled;
mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert;
mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin;
mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert;
mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey;
JsonObject mqtt_hass = mqtt.createNestedObject("hass");
JsonObject mqtt_hass = mqtt["hass"].to<JsonObject>();
mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled;
mqtt_hass["retain"] = config.Mqtt.Hass.Retain;
mqtt_hass["topic"] = config.Mqtt.Hass.Topic;
mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels;
mqtt_hass["expire"] = config.Mqtt.Hass.Expire;
JsonObject dtu = doc.createNestedObject("dtu");
JsonObject dtu = doc["dtu"].to<JsonObject>();
dtu["serial"] = config.Dtu.Serial;
dtu["poll_interval"] = config.Dtu.PollInterval;
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
@ -97,14 +93,14 @@ bool ConfigurationClass::write()
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode;
JsonObject security = doc.createNestedObject("security");
JsonObject security = doc["security"].to<JsonObject>();
security["password"] = config.Security.Password;
security["allow_readonly"] = config.Security.AllowReadonly;
JsonObject device = doc.createNestedObject("device");
JsonObject device = doc["device"].to<JsonObject>();
device["pinmapping"] = config.Dev_PinMapping;
JsonObject display = device.createNestedObject("display");
JsonObject display = device["display"].to<JsonObject>();
display["powersafe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver;
display["rotation"] = config.Display.Rotation;
@ -113,15 +109,15 @@ bool ConfigurationClass::write()
display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode;
JsonArray leds = device.createNestedArray("led");
JsonArray leds = device["led"].to<JsonArray>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds.createNestedObject();
JsonObject led = leds.add<JsonObject>();
led["brightness"] = config.Led_Single[i].Brightness;
}
JsonArray inverters = doc.createNestedArray("inverters");
JsonArray inverters = doc["inverters"].to<JsonArray>();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters.createNestedObject();
JsonObject inv = inverters.add<JsonObject>();
inv["serial"] = config.Inverter[i].Serial;
inv["name"] = config.Inverter[i].Name;
inv["order"] = config.Inverter[i].Order;
@ -134,15 +130,19 @@ bool ConfigurationClass::write()
inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight;
inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection;
JsonArray channel = inv.createNestedArray("channel");
JsonArray channel = inv["channel"].to<JsonArray>();
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
JsonObject chanData = channel.createNestedObject();
JsonObject chanData = channel.add<JsonObject>();
chanData["name"] = config.Inverter[i].channel[c].Name;
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
}
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
// Serialize JSON to file
if (serializeJson(doc, f) == 0) {
MessageOutput.println("Failed to write file");
@ -157,11 +157,7 @@ bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
@ -169,6 +165,10 @@ bool ConfigurationClass::read()
MessageOutput.println("Failed to read file, using default configuration");
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
JsonObject cfg = doc["cfg"];
config.Cfg.Version = cfg["version"] | CONFIG_VERSION;
config.Cfg.SaveCount = cfg["save_count"] | 0;
@ -324,11 +324,7 @@ void ConfigurationClass::migrate()
return;
}
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
@ -337,6 +333,10 @@ void ConfigurationClass::migrate()
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
if (config.Cfg.Version < 0x00011700) {
JsonArray inverters = doc["inverters"];
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
@ -372,6 +372,12 @@ void ConfigurationClass::migrate()
config.Dtu.Cmt.Frequency *= 1000;
}
if (config.Cfg.Version < 0x00011c00) {
if (!strcmp(config.Ntp.Server, NTP_SERVER_OLD)) {
strlcpy(config.Ntp.Server, NTP_SERVER, sizeof(config.Ntp.Server));
}
}
f.close();
config.Cfg.Version = CONFIG_VERSION;

View File

@ -51,9 +51,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
if (PinMapping.isValidCmt2300Config()) {
Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3);
MessageOutput.println(F(" Setting country mode... "));
MessageOutput.println(" Setting country mode... ");
Hoymiles.getRadioCmt()->setCountryMode(static_cast<CountryModeId_t>(config.Dtu.Cmt.CountryMode));
MessageOutput.println(F(" Setting CMT target frequency... "));
MessageOutput.println(" Setting CMT target frequency... ");
Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
}

View File

@ -137,10 +137,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
name = "CH" + chanNum + " " + fieldName;
}
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = name;
root["stat_t"] = stateTopic;
@ -163,6 +160,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
root["stat_cla"] = stateCls;
}
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
@ -185,10 +186,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId;
@ -204,6 +202,10 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
@ -227,10 +229,7 @@ void MqttHandleHassClass::publishInverterNumber(
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic;
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId;
@ -246,6 +245,10 @@ void MqttHandleHassClass::publishInverterNumber(
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
@ -265,10 +268,7 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
@ -278,6 +278,10 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
@ -293,10 +297,7 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
topic = id;
}
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
@ -322,6 +323,10 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
@ -339,10 +344,7 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
topic = String("dtu/") + "/" + id;
}
DynamicJsonDocument root(1024);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
@ -359,13 +361,17 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::shared_ptr<InverterAbstract> inv)
void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr<InverterAbstract> inv)
{
createDeviceInfo(
root,
@ -378,7 +384,7 @@ void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::sha
getDtuUniqueId());
}
void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root)
void MqttHandleHassClass::createDtuInfo(JsonDocument& root)
{
createDeviceInfo(
root,
@ -391,12 +397,12 @@ void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root)
}
void MqttHandleHassClass::createDeviceInfo(
DynamicJsonDocument& root,
JsonDocument& root,
const String& name, const String& identifiers, const String& configuration_url,
const String& manufacturer, const String& model, const String& sw_version,
const String& via_device)
{
auto object = root.createNestedObject("dev");
auto object = root["dev"].to<JsonObject>();
object["name"] = name;
object["ids"] = identifiers;

View File

@ -8,8 +8,6 @@
#include <LittleFS.h>
#include <string.h>
#define JSON_BUFFER_SIZE 6144
#ifndef DISPLAY_TYPE
#define DISPLAY_TYPE 0U
#endif
@ -141,7 +139,7 @@ bool PinMappingClass::init(const String& deviceMapping)
return false;
}
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
JsonDocument doc;
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, f);
if (error) {
@ -216,4 +214,4 @@ bool PinMappingClass::isValidCmt2300Config() const
bool PinMappingClass::isValidEthConfig() const
{
return _pinMapping.eth_enabled;
}
}

View File

@ -69,9 +69,9 @@ void Utils::restartDtu()
ESP.restart();
}
bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line)
bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line)
{
if (doc.capacity() == 0) {
if (doc.overflowed()) {
MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line);
return false;
}

View File

@ -4,6 +4,7 @@
*/
#include "WebApi.h"
#include "Configuration.h"
#include "MessageOutput.h"
#include "defaults.h"
#include <AsyncJson.h>
@ -85,4 +86,58 @@ void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const
}
}
bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document)
{
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return false;
}
const String json = request->getParam("data", true)->value();
const DeserializationError error = deserializeJson(json_document, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return false;
}
return true;
}
uint64_t WebApiClass::parseSerialFromRequest(AsyncWebServerRequest* request, String param_name)
{
if (request->hasParam(param_name)) {
String s = request->getParam(param_name)->value();
return strtoll(s.c_str(), NULL, 16);
}
return 0;
}
bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line)
{
bool ret_val = true;
if (response->overflowed()) {
auto& root = response->getRoot();
root.clear();
root["message"] = String("500 Internal Server Error: ") + function + ", " + line;
root["code"] = WebApiError::GenericInternalServerError;
root["type"] = "danger";
response->setCode(500);
MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line);
ret_val = false;
}
response->setLength();
request->send(response);
return ret_val;
}
WebApiClass WebApi;

View File

@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
requestFile = name;
} else {
request->send(404);
return;
}
}
@ -53,51 +54,24 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("delete"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["delete"].as<bool>() == false) {
retMsg["message"] = "Not deleted anything!";
retMsg["code"] = WebApiError::ConfigNotDeleted;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -105,8 +79,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
retMsg["message"] = "Configuration resettet. Rebooting now...";
retMsg["code"] = WebApiError::ConfigSuccess;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Utils::removeAllFiles();
Utils::restartDtu();
@ -120,7 +93,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
auto data = root.createNestedArray("configs");
auto data = root["configs"].to<JsonArray>();
File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile();
@ -128,15 +101,14 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
if (file.isDirectory()) {
continue;
}
JsonObject obj = data.createNestedObject();
JsonObject obj = data.add<JsonObject>();
obj["name"] = String(file.name());
file = rootfs.openNextFile();
}
file.close();
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)

View File

@ -26,15 +26,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();
const PinMapping_t& pin = PinMapping.get();
auto curPin = root.createNestedObject("curPin");
auto curPin = root["curPin"].to<JsonObject>();
curPin["name"] = config.Dev_PinMapping;
auto nrfPinObj = curPin.createNestedObject("nrf24");
auto nrfPinObj = curPin["nrf24"].to<JsonObject>();
nrfPinObj["clk"] = pin.nrf24_clk;
nrfPinObj["cs"] = pin.nrf24_cs;
nrfPinObj["en"] = pin.nrf24_en;
@ -42,7 +42,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
nrfPinObj["miso"] = pin.nrf24_miso;
nrfPinObj["mosi"] = pin.nrf24_mosi;
auto cmtPinObj = curPin.createNestedObject("cmt");
auto cmtPinObj = curPin["cmt"].to<JsonObject>();
cmtPinObj["clk"] = pin.cmt_clk;
cmtPinObj["cs"] = pin.cmt_cs;
cmtPinObj["fcs"] = pin.cmt_fcs;
@ -50,7 +50,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
cmtPinObj["gpio2"] = pin.cmt_gpio2;
cmtPinObj["gpio3"] = pin.cmt_gpio3;
auto ethPinObj = curPin.createNestedObject("eth");
auto ethPinObj = curPin["eth"].to<JsonObject>();
ethPinObj["enabled"] = pin.eth_enabled;
ethPinObj["phy_addr"] = pin.eth_phy_addr;
ethPinObj["power"] = pin.eth_power;
@ -59,19 +59,19 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
ethPinObj["type"] = pin.eth_type;
ethPinObj["clk_mode"] = pin.eth_clk_mode;
auto displayPinObj = curPin.createNestedObject("display");
auto displayPinObj = curPin["display"].to<JsonObject>();
displayPinObj["type"] = pin.display_type;
displayPinObj["data"] = pin.display_data;
displayPinObj["clk"] = pin.display_clk;
displayPinObj["cs"] = pin.display_cs;
displayPinObj["reset"] = pin.display_reset;
auto ledPinObj = curPin.createNestedObject("led");
auto ledPinObj = curPin["led"].to<JsonObject>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
ledPinObj["led" + String(i)] = pin.led[i];
}
auto display = root.createNestedObject("display");
auto display = root["display"].to<JsonObject>();
display["rotation"] = config.Display.Rotation;
display["power_safe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver;
@ -80,14 +80,13 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["diagramduration"] = config.Display.Diagram.Duration;
display["diagrammode"] = config.Display.Diagram.Mode;
auto leds = root.createNestedArray("led");
auto leds = root["led"].to<JsonArray>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
auto led = leds.createNestedObject();
auto led = leds.add<JsonObject>();
led["brightness"] = config.Led_Single[i].Brightness;
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
@ -96,45 +95,19 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > MQTT_JSON_DOC_SIZE) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("curPin")
|| root.containsKey("display"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -142,8 +115,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::HardwarePinMappingLength;
retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -174,8 +146,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
if (performRestart) {
Utils::restartDtu();

View File

@ -23,13 +23,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto serial = WebApi.parseSerialFromRequest(request);
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
@ -43,6 +37,5 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr();
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -62,10 +62,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
root["cmt_country"] = config.Dtu.Cmt.CountryMode;
root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth();
auto data = root.createNestedArray("country_def");
auto data = root["country_def"].to<JsonArray>();
auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList();
for (const auto& definition : countryDefs) {
auto obj = data.createNestedObject();
auto obj = data.add<JsonObject>();
obj["freq_default"] = definition.definition.Freq_Default;
obj["freq_min"] = definition.definition.Freq_Min;
obj["freq_max"] = definition.definition.Freq_Max;
@ -73,8 +73,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
obj["freq_legal_max"] = definition.definition.Freq_Legal_Max;
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
@ -84,37 +83,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("serial")
&& root.containsKey("pollinterval")
@ -124,8 +98,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("cmt_country"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -135,40 +108,35 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
if (serial == 0) {
retMsg["message"] = "Serial cannot be zero!";
retMsg["code"] = WebApiError::DtuSerialZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["pollinterval"].as<uint32_t>() == 0) {
retMsg["message"] = "Poll interval must be greater zero!";
retMsg["code"] = WebApiError::DtuPollZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["nrf_palevel"].as<uint8_t>() > 3) {
retMsg["message"] = "Invalid power level setting!";
retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["cmt_palevel"].as<int8_t>() < -10 || root["cmt_palevel"].as<int8_t>() > 20) {
retMsg["message"] = "Invalid power level setting!";
retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["cmt_country"].as<uint8_t>() >= CountryModeId_t::CountryModeId_Max) {
retMsg["message"] = "Invalid country setting!";
retMsg["code"] = WebApiError::DtuInvalidCmtCountry;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -181,8 +149,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::DtuInvalidCmtFrequency;
retMsg["param"]["min"] = FrequencyDefinition.Freq_Min;
retMsg["param"]["max"] = FrequencyDefinition.Freq_Max;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -197,8 +164,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
_applyDataTask.enable();
_applyDataTask.restart();
}

View File

@ -20,14 +20,9 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto serial = WebApi.parseSerialFromRequest(request);
AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN;
if (request->hasParam("locale")) {
@ -47,10 +42,10 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
uint8_t logEntryCount = inv->EventLog()->getEntryCount();
root["count"] = logEntryCount;
JsonArray eventsArray = root.createNestedArray("events");
JsonArray eventsArray = root["events"].to<JsonArray>();
for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) {
JsonObject eventsObject = eventsArray.createNestedObject();
JsonObject eventsObject = eventsArray.add<JsonObject>();
AlarmLogEntry_t entry;
inv->EventLog()->getLogEntry(logEntry, entry, locale);
@ -62,6 +57,5 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
}
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -21,32 +21,26 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto serial = WebApi.parseSerialFromRequest(request);
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
root["name"] = inv->GridProfile()->getProfileName();
root["version"] = inv->GridProfile()->getProfileVersion();
auto jsonSections = root.createNestedArray("sections");
auto jsonSections = root["sections"].to<JsonArray>();
auto profSections = inv->GridProfile()->getProfile();
for (auto &profSection : profSections) {
auto jsonSection = jsonSections.createNestedObject();
auto jsonSection = jsonSections.add<JsonObject>();
jsonSection["name"] = profSection.SectionName;
auto jsonItems = jsonSection.createNestedArray("items");
auto jsonItems = jsonSection["items"].to<JsonArray>();
for (auto &profItem : profSection.items) {
auto jsonItem = jsonItems.createNestedObject();
auto jsonItem = jsonItems.add<JsonObject>();
jsonItem["n"] = profItem.Name;
jsonItem["u"] = profItem.Unit;
@ -55,8 +49,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request)
}
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request)
@ -65,24 +58,17 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto serial = WebApi.parseSerialFromRequest(request);
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
auto raw = root.createNestedArray("raw");
auto raw = root["raw"].to<JsonArray>();
auto data = inv->GridProfile()->getRawData();
copyArray(&data[0], data.size(), raw);
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -29,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
JsonArray data = root.createNestedArray("inverter");
JsonArray data = root["inverter"].to<JsonArray>();
const CONFIG_T& config = Configuration.get();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) {
JsonObject obj = data.createNestedObject();
JsonObject obj = data.add<JsonObject>();
obj["id"] = i;
obj["name"] = String(config.Inverter[i].Name);
obj["order"] = config.Inverter[i].Order;
@ -67,9 +67,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size();
}
JsonArray channel = obj.createNestedArray("channel");
JsonArray channel = obj["channel"].to<JsonArray>();
for (uint8_t c = 0; c < max_channels; c++) {
JsonObject chanData = channel.createNestedObject();
JsonObject chanData = channel.add<JsonObject>();
chanData["name"] = config.Inverter[i].channel[c].Name;
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
@ -77,8 +77,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
}
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
@ -88,44 +87,18 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("serial")
&& root.containsKey("name"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -135,8 +108,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -144,8 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::InverterNameLength;
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -155,8 +126,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!";
retMsg["code"] = WebApiError::InverterCount;
retMsg["param"]["max"] = INV_MAX_COUNT;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -167,8 +137,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!");
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial);
@ -188,51 +157,24 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
retMsg["message"] = "Invalid ID specified!";
retMsg["code"] = WebApiError::InverterInvalidId;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -242,8 +184,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -251,8 +192,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::InverterNameLength;
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -260,8 +200,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) {
retMsg["message"] = "Invalid amount of max channel setting given!";
retMsg["code"] = WebApiError::InverterInvalidMaxChannel;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -293,8 +232,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!");
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial);
@ -333,51 +271,24 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("id"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
retMsg["message"] = "Invalid ID specified!";
retMsg["code"] = WebApiError::InverterInvalidId;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -390,8 +301,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
MqttHandleHass.forceUpdate();
}
@ -403,43 +313,17 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("order"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -457,6 +341,5 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -47,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
root[serial]["limit_set_status"] = limitStatus;
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
@ -58,45 +57,19 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("serial")
&& root.containsKey("limit_value")
&& root.containsKey("limit_type"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -106,8 +79,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::LimitSerialZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -115,8 +87,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!";
retMsg["code"] = WebApiError::LimitInvalidLimit;
retMsg["param"]["max"] = MAX_INVERTER_LIMIT;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -127,8 +98,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Invalid type specified!";
retMsg["code"] = WebApiError::LimitInvalidType;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -139,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
if (inv == nullptr) {
retMsg["message"] = "Invalid inverter specified!";
retMsg["code"] = WebApiError::LimitInvalidInverter;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -150,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -22,44 +22,18 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > MQTT_JSON_DOC_SIZE) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("reboot"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -68,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
retMsg["message"] = "Reboot triggered!";
retMsg["code"] = WebApiError::MaintenanceRebootTriggered;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Utils::restartDtu();
} else {
retMsg["message"] = "Reboot cancled!";
retMsg["code"] = WebApiError::MaintenanceRebootCancled;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
}

View File

@ -26,7 +26,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();
@ -50,8 +50,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic;
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
@ -60,7 +59,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();
@ -88,8 +87,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic;
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
@ -98,38 +96,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > MQTT_JSON_DOC_SIZE) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("mqtt_enabled")
&& root.containsKey("mqtt_hostname")
@ -155,8 +128,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("mqtt_hass_individualpanels"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -165,8 +137,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::MqttHostnameLength;
retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -174,48 +145,42 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttUsernameLength;
retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_password"].as<String>().length() > MQTT_MAX_PASSWORD_STRLEN) {
retMsg["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttPasswordLength;
retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
retMsg["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "Topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttTopicCharacter;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (!root["mqtt_topic"].as<String>().endsWith("/")) {
retMsg["message"] = "Topic must end with a slash (/)!";
retMsg["code"] = WebApiError::MqttTopicTrailingSlash;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_port"].as<uint>() == 0 || root["mqtt_port"].as<uint>() > 65535) {
retMsg["message"] = "Port must be a number between 1 and 65535!";
retMsg["code"] = WebApiError::MqttPort;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -225,8 +190,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttCertificateLength;
retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -234,16 +198,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_lwt_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "LWT topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttLwtTopicCharacter;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -251,8 +213,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtOnlineLength;
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -260,8 +221,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtOfflineLength;
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -269,8 +229,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!";
retMsg["code"] = WebApiError::MqttLwtQos;
retMsg["param"]["max"] = 2;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -279,8 +238,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::MqttPublishInterval;
retMsg["param"]["min"] = 5;
retMsg["param"]["max"] = 65535;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -289,16 +247,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttHassTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["mqtt_hass_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "Hass topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttHassTopicCharacter;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
}
@ -331,8 +287,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
MqttSettings.performReconnect();
MqttHandleHass.forceUpdate();

View File

@ -46,8 +46,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
root["ap_mac"] = WiFi.softAPmacAddress();
root["ap_stationnum"] = WiFi.softAPgetStationNum();
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
@ -72,8 +71,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
root["aptimeout"] = config.WiFi.ApTimeout;
root["mdnsenabled"] = config.Mdns.Enabled;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
@ -83,37 +81,12 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("ssid")
&& root.containsKey("password")
@ -127,8 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("aptimeout"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -136,68 +108,59 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
if (!ipaddress.fromString(root["ipaddress"].as<String>())) {
retMsg["message"] = "IP address is invalid!";
retMsg["code"] = WebApiError::NetworkIpInvalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
IPAddress netmask;
if (!netmask.fromString(root["netmask"].as<String>())) {
retMsg["message"] = "Netmask is invalid!";
retMsg["code"] = WebApiError::NetworkNetmaskInvalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
IPAddress gateway;
if (!gateway.fromString(root["gateway"].as<String>())) {
retMsg["message"] = "Gateway is invalid!";
retMsg["code"] = WebApiError::NetworkGatewayInvalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
IPAddress dns1;
if (!dns1.fromString(root["dns1"].as<String>())) {
retMsg["message"] = "DNS Server IP 1 is invalid!";
retMsg["code"] = WebApiError::NetworkDns1Invalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
IPAddress dns2;
if (!dns2.fromString(root["dns2"].as<String>())) {
retMsg["message"] = "DNS Server IP 2 is invalid!";
retMsg["code"] = WebApiError::NetworkDns2Invalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["hostname"].as<String>().length() == 0 || root["hostname"].as<String>().length() > WIFI_MAX_HOSTNAME_STRLEN) {
retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!";
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
if (root["ssid"].as<String>().length() == 0 || root["ssid"].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!";
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
}
if (root["password"].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
if (root["aptimeout"].as<uint>() > 99999) {
retMsg["message"] = "ApTimeout must be a number between 0 and 99999!";
retMsg["code"] = WebApiError::NetworkApTimeoutInvalid;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -235,8 +198,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
NetworkSettings.enableAdminMode();
NetworkSettings.applyConfig();

View File

@ -63,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable();
root["sun_isDayPeriod"] = SunPosition.isDayPeriod();
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
@ -84,8 +83,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
root["latitude"] = config.Ntp.Latitude;
root["sunsettype"] = config.Ntp.SunsetType;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
@ -95,37 +93,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("ntp_server")
&& root.containsKey("ntp_timezone")
@ -134,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("sunsettype"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -143,8 +115,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpServerLength;
retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -152,8 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpTimezoneLength;
retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -161,8 +131,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength;
retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -176,8 +145,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
NtpSettings.setServer();
NtpSettings.setTimezone();
@ -208,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
root["minute"] = timeinfo.tm_min;
root["second"] = timeinfo.tm_sec;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
@ -219,37 +186,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("year")
&& root.containsKey("month")
@ -259,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
&& root.containsKey("second"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -269,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpYearInvalid;
retMsg["param"]["min"] = 2022;
retMsg["param"]["max"] = 2100;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -279,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpMonthInvalid;
retMsg["param"]["min"] = 1;
retMsg["param"]["max"] = 12;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -289,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpDayInvalid;
retMsg["param"]["min"] = 1;
retMsg["param"]["max"] = 31;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -299,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpHourInvalid;
retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 23;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -309,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpMinuteInvalid;
retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 59;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -319,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpSecondInvalid;
retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 59;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -341,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["message"] = "Time updated!";
retMsg["code"] = WebApiError::NtpTimeUpdated;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -40,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
root[inv->serialString()]["power_set_status"] = limitStatus;
}
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
@ -51,45 +50,19 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("serial")
&& (root.containsKey("power")
|| root.containsKey("restart")))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -99,8 +72,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::PowerSerialZero;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -108,8 +80,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
if (inv == nullptr) {
retMsg["message"] = "Invalid inverter specified!";
retMsg["code"] = WebApiError::PowerInvalidInverter;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -126,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -31,8 +31,7 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request)
root["password"] = config.Security.Password;
root["allow_readonly"] = config.Security.AllowReadonly;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
@ -42,44 +41,18 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
const String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
const DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!root.containsKey("password")
&& root.containsKey("allow_readonly")) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -87,8 +60,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
retMsg["code"] = WebApiError::SecurityPasswordLength;
retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
@ -98,8 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
@ -114,6 +85,5 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
retMsg["message"] = "Authentication successful!";
retMsg["code"] = WebApiError::SecurityAuthSuccess;
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -76,6 +76,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root["cmt_configured"] = PinMapping.isValidCmt2300Config();
root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected();
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -73,19 +73,20 @@ void WebApiWsLiveClass::sendDataTaskCb()
try {
std::lock_guard<std::mutex> lock(_mutex);
DynamicJsonDocument root(4096);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
continue;
}
JsonDocument root;
JsonVariant var = root;
auto invArray = var.createNestedArray("inverters");
auto invObject = invArray.createNestedObject();
auto invArray = var["inverters"].to<JsonArray>();
auto invObject = invArray.add<JsonObject>();
generateCommonJsonResponse(var);
generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
continue;
}
String buffer;
serializeJson(root, buffer);
@ -101,12 +102,12 @@ void WebApiWsLiveClass::sendDataTaskCb()
void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
{
JsonObject totalObj = root.createNestedObject("total");
auto totalObj = root["total"].to<JsonObject>();
addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits());
addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits());
addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits());
JsonObject hintObj = root.createNestedObject("hints");
JsonObject hintObj = root["hints"].to<JsonObject>();
struct tm timeinfo;
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected()));
@ -144,7 +145,7 @@ void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, st
// Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) {
JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t));
auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to<JsonObject>();
for (auto& c : inv->Statistics()->getChannelsByType(t)) {
if (t == TYPE_DC) {
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
@ -221,21 +222,15 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
try {
std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096);
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
JsonArray invArray = root.createNestedArray("inverters");
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto invArray = root["inverters"].to<JsonArray>();
auto serial = WebApi.parseSerialFromRequest(request);
if (serial > 0) {
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
JsonObject invObject = invArray.createNestedObject();
JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv);
}
@ -247,15 +242,14 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
continue;
}
JsonObject invObject = invArray.createNestedObject();
JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv);
}
}
generateCommonJsonResponse(root);
response->setLength();
request->send(response);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
} catch (const std::bad_alloc& bad_alloc) {
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());

View File

@ -1,14 +0,0 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

36
webapp/eslint.config.js Normal file
View File

@ -0,0 +1,36 @@
/* eslint-env node */
import path from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import pluginVue from 'eslint-plugin-vue'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
export default [
js.configs.recommended,
...pluginVue.configs['flat/essential'],
...compat.extends("@vue/eslint-config-typescript/recommended"),
{
files: [
"**/*.vue",
"**/*.js",
"**/*.jsx",
"**/*.cjs",
"**/*.mjs",
"**/*.ts",
"**/*.tsx",
"**/*.cts",
"**/*.mts",
],
languageOptions: {
ecmaVersion: 'latest'
},
}
]

View File

@ -9,7 +9,7 @@
"preview": "vite preview --port 4173",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"lint": "eslint ."
},
"dependencies": {
"@popperjs/core": "^2.11.8",
@ -18,32 +18,31 @@
"mitt": "^3.0.1",
"sortablejs": "^1.15.2",
"spark-md5": "^3.0.2",
"vue": "^3.4.21",
"vue-i18n": "^9.10.2",
"vue-router": "^4.3.0"
"vue": "^3.4.25",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.2"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^3.0.1",
"@rushstack/eslint-patch": "^1.7.2",
"@tsconfig/node18": "^18.2.2",
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@tsconfig/node18": "^18.2.4",
"@types/bootstrap": "^5.2.10",
"@types/node": "^20.11.28",
"@types/node": "^20.12.7",
"@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.8",
"@types/spark-md5": "^3.0.4",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"eslint": "^9.1.1",
"eslint-plugin-vue": "^9.25.0",
"npm-run-all": "^4.1.5",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.72.0",
"terser": "^5.29.2",
"typescript": "^5.4.2",
"vite": "^5.1.6",
"sass": "^1.75.0",
"terser": "^5.30.4",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.4.0",
"vue-tsc": "^2.0.6"
"vite-plugin-css-injected-by-js": "^3.5.0",
"vue-tsc": "^2.0.14"
}
}

View File

@ -180,7 +180,7 @@
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo":"AST4",
"America/Sao_Paulo":"<-03>3",
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0",
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy":"AST4",
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
@ -200,7 +200,7 @@
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey":"<+11>-11",
"Antarctica/Casey":"<+08>-8",
"Antarctica/Davis":"<+07>-7",
"Antarctica/DumontDUrville":"<+10>-10",
"Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
@ -210,10 +210,10 @@
"Antarctica/Rothera":"<-03>3",
"Antarctica/Syowa":"<+03>-3",
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok":"<+06>-6",
"Antarctica/Vostok":"<+05>-5",
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden":"<+03>-3",
"Asia/Almaty":"<+06>-6",
"Asia/Almaty":"<+05>-5",
"Asia/Amman":"<+03>-3",
"Asia/Anadyr":"<+12>-12",
"Asia/Aqtau":"<+05>-5",

View File

@ -48,15 +48,14 @@ export default defineComponent({
showReload: { type: Boolean, required: false, default: false },
},
mounted() {
var self = this;
console.log("init");
PullToRefresh.init({
mainElement: 'body', // above which element?
instructionsPullToRefresh: this.$t('base.Pull'),
instructionsReleaseToRefresh: this.$t('base.Release'),
instructionsRefreshing: this.$t('base.Refreshing'),
onRefresh: function() {
self.$emit('reload');
onRefresh: () => {
this.$emit('reload');
}
});
},

View File

@ -52,7 +52,7 @@ export default defineComponent({
_countDownTimeout = undefined;
};
var countDown = ref();
const countDown = ref();
watch(() => props.modelValue, () => {
countDown.value = parseCountDown(props.modelValue);
});
@ -116,4 +116,4 @@ export default defineComponent({
};
},
});
</script>
</script>

View File

@ -83,10 +83,10 @@ export default defineComponent({
},
computed: {
modelAllowVersionInfo: {
get(): any {
get(): boolean {
return !!this.allowVersionInfo;
},
set(value: any) {
set(value: boolean) {
this.$emit('update:allowVersionInfo', value);
},
},

View File

@ -83,10 +83,12 @@ export default defineComponent({
},
computed: {
model: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(): any {
if (this.type === 'checkbox') return !!this.modelValue;
return this.modelValue;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(value: any) {
this.$emit('update:modelValue', value);
},
@ -112,4 +114,4 @@ export default defineComponent({
}
},
});
</script>
</script>

View File

@ -28,9 +28,11 @@ export default defineComponent({
},
computed: {
model: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(): any {
return this.modelValue;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(value: any) {
this.$emit('update:modelValue', value);
},

View File

@ -146,8 +146,8 @@ export default defineComponent({
},
isEaster() {
const easter = this.getEasterSunday(this.now.getFullYear());
var easterStart = new Date(easter);
var easterEnd = new Date(easter);
const easterStart = new Date(easter);
const easterEnd = new Date(easter);
easterStart.setDate(easterStart.getDate() - 2);
easterEnd.setDate(easterEnd.getDate() + 1);
return this.now >= easterStart && this.now < easterEnd;
@ -170,18 +170,18 @@ export default defineComponent({
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");
},
getEasterSunday(year: number): Date {
var f = Math.floor;
var G = year % 19;
var C = f(year / 100);
var H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30;
var I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11));
var J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7;
var L = I - J;
var month = 3 + f((L + 40) / 44);
var day = L + 28 - 31 * f(month / 4);
const f = Math.floor;
const G = year % 19;
const C = f(year / 100);
const H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30;
const I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11));
const J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7;
const L = I - J;
const month = 3 + f((L + 40) / 44);
const day = L + 28 - 31 * f(month / 4);
return new Date(year, month - 1, day);
}
},
});
</script>
</script>

View File

@ -84,9 +84,11 @@ export default defineComponent({
let comCur = 999999;
if (this.selectedPinAssignment && category in this.selectedPinAssignment) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
comSel = (this.selectedPinAssignment as any)[category][prop];
}
if (this.currentPinAssignment && category in this.currentPinAssignment) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
comCur = (this.currentPinAssignment as any)[category][prop];
}

View File

@ -32,6 +32,9 @@
"Release": "Loslassen zum Aktualisieren",
"Close": "Schließen"
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Dunkel",
"Light": "Hell",

View File

@ -32,6 +32,9 @@
"Release": "Release to refresh",
"Close": "Close"
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Dark",
"Light": "Light",

View File

@ -32,6 +32,9 @@
"Release": "Release to refresh",
"Close": "Fermer"
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Sombre",
"Light": "Clair",

View File

@ -3,6 +3,7 @@ import ConfigAdminView from '@/views/ConfigAdminView.vue';
import ConsoleInfoView from '@/views/ConsoleInfoView.vue';
import DeviceAdminView from '@/views/DeviceAdminView.vue'
import DtuAdminView from '@/views/DtuAdminView.vue';
import ErrorView from '@/views/ErrorView.vue';
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
import HomeView from '@/views/HomeView.vue';
import InverterAdminView from '@/views/InverterAdminView.vue';
@ -32,6 +33,11 @@ const router = createRouter({
name: 'Login',
component: LoginView
},
{
path: '/error?status=:status&message=:message',
name: 'Error',
component: ErrorView
},
{
path: '/about',
name: 'About',
@ -115,4 +121,4 @@ const router = createRouter({
]
});
export default router;
export default router;

View File

@ -41,7 +41,7 @@ export function isLoggedIn(): boolean {
return (localStorage.getItem('user') != null);
}
export function login(username: String, password: String) {
export function login(username: string, password: string) {
const requestOptions = {
method: 'GET',
headers: {
@ -65,7 +65,7 @@ export function login(username: String, password: String) {
});
}
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>, router: Router) {
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>, router: Router, ignore_error: boolean = false) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
@ -74,9 +74,13 @@ export function handleResponse(response: Response, emitter: Emitter<Record<Event
logout();
emitter.emit("logged-out");
router.push({ path: "/login", query: { returnUrl: router.currentRoute.value.fullPath } });
return Promise.reject();
}
const error = { message: (data && data.message) || response.statusText, status: response.status || 0 };
if (!ignore_error) {
router.push({ name: "Error", params: error });
}
return Promise.reject(error);
}
@ -99,4 +103,4 @@ function handleAuthResponse(response: Response) {
return data;
});
}
}

View File

@ -188,8 +188,8 @@ export default defineComponent({
fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() })
.then(res => res.blob())
.then(blob => {
var file = window.URL.createObjectURL(blob);
var a = document.createElement('a');
const file = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = file;
a.download = this.backupFileSelect;
document.body.appendChild(a);

View File

@ -1,154 +1,154 @@
<template>
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
<div class="row g-3 align-items-center">
<div class="col">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
v-model="isAutoScroll">
<label class="form-check-label" for="autoScroll">
{{ $t('console.EnableAutoScroll') }}
</label>
</div>
</div>
<div class="col text-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" :onClick="clearConsole">
{{ $t('console.ClearConsole') }}</button>
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
{{ $t('console.CopyToClipboard') }}</button>
</div>
</div>
</div>
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
</CardElement>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import CardElement from '@/components/CardElement.vue';
import { authUrl } from '@/utils/authentication';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
CardElement,
},
data() {
return {
socket: {} as WebSocket,
heartInterval: 0,
dataLoading: true,
consoleBuffer: "",
isAutoScroll: true,
endWithNewline: false,
};
},
created() {
this.initSocket();
this.dataLoading = false;
},
unmounted() {
this.closeSocket();
},
watch: {
consoleBuffer() {
if (this.isAutoScroll) {
let textarea = this.$el.querySelector("#console");
setTimeout(() => {
textarea.scrollTop = textarea.scrollHeight;
}, 0);
}
}
},
methods: {
initSocket() {
console.log("Starting connection to WebSocket Server");
const { protocol, host } = location;
const authString = authUrl();
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
}://${authString}${host}/console`;
this.closeSocket();
this.socket = new WebSocket(webSocketUrl);
this.socket.onmessage = (event) => {
console.log(event);
let outstr = new String(event.data);
let removedNewline = false;
if (outstr.endsWith('\n')) {
outstr = outstr.substring(0, outstr.length - 1);
removedNewline = true;
}
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
this.endWithNewline = removedNewline;
this.heartCheck(); // Reset heartbeat detection
};
this.socket.onopen = function (event) {
console.log(event);
console.log("Successfully connected to the echo websocket server...");
};
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
window.onbeforeunload = () => {
this.closeSocket();
};
},
// Send heartbeat packets regularly * 59s Send a heartbeat
heartCheck() {
this.heartInterval && clearTimeout(this.heartInterval);
this.heartInterval = setInterval(() => {
if (this.socket.readyState === 1) {
// Connection status
this.socket.send("ping");
} else {
this.initSocket(); // Breakpoint reconnection 5 Time
}
}, 5 * 1000);
},
/** To break off websocket Connect */
closeSocket() {
try {
this.socket.close();
} catch {
// continue regardless of error
}
this.heartInterval && clearTimeout(this.heartInterval);
},
getOutDate(): String {
const u = new Date();
return ('0' + u.getHours()).slice(-2) + ':' +
('0' + u.getMinutes()).slice(-2) + ':' +
('0' + u.getSeconds()).slice(-2) + '.' +
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
},
clearConsole() {
this.consoleBuffer = "";
},
copyConsole() {
var input = document.createElement('textarea');
input.innerHTML = this.consoleBuffer;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
}
});
</script>
<style>
#console {
background-color: #0C0C0C;
color: #CCCCCC;
padding: 8px;
font-family: courier new;
font-size: .875em;
}
</style>
<template>
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
<div class="row g-3 align-items-center">
<div class="col">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
v-model="isAutoScroll">
<label class="form-check-label" for="autoScroll">
{{ $t('console.EnableAutoScroll') }}
</label>
</div>
</div>
<div class="col text-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" :onClick="clearConsole">
{{ $t('console.ClearConsole') }}</button>
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
{{ $t('console.CopyToClipboard') }}</button>
</div>
</div>
</div>
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
</CardElement>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import CardElement from '@/components/CardElement.vue';
import { authUrl } from '@/utils/authentication';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
CardElement,
},
data() {
return {
socket: {} as WebSocket,
heartInterval: 0,
dataLoading: true,
consoleBuffer: "",
isAutoScroll: true,
endWithNewline: false,
};
},
created() {
this.initSocket();
this.dataLoading = false;
},
unmounted() {
this.closeSocket();
},
watch: {
consoleBuffer() {
if (this.isAutoScroll) {
const textarea = this.$el.querySelector("#console");
setTimeout(() => {
textarea.scrollTop = textarea.scrollHeight;
}, 0);
}
}
},
methods: {
initSocket() {
console.log("Starting connection to WebSocket Server");
const { protocol, host } = location;
const authString = authUrl();
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
}://${authString}${host}/console`;
this.closeSocket();
this.socket = new WebSocket(webSocketUrl);
this.socket.onmessage = (event) => {
console.log(event);
let outstr = new String(event.data);
let removedNewline = false;
if (outstr.endsWith('\n')) {
outstr = outstr.substring(0, outstr.length - 1);
removedNewline = true;
}
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
this.endWithNewline = removedNewline;
this.heartCheck(); // Reset heartbeat detection
};
this.socket.onopen = function (event) {
console.log(event);
console.log("Successfully connected to the echo websocket server...");
};
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
window.onbeforeunload = () => {
this.closeSocket();
};
},
// Send heartbeat packets regularly * 59s Send a heartbeat
heartCheck() {
this.heartInterval && clearTimeout(this.heartInterval);
this.heartInterval = setInterval(() => {
if (this.socket.readyState === 1) {
// Connection status
this.socket.send("ping");
} else {
this.initSocket(); // Breakpoint reconnection 5 Time
}
}, 5 * 1000);
},
/** To break off websocket Connect */
closeSocket() {
try {
this.socket.close();
} catch {
// continue regardless of error
}
this.heartInterval && clearTimeout(this.heartInterval);
},
getOutDate(): string {
const u = new Date();
return ('0' + u.getHours()).slice(-2) + ':' +
('0' + u.getMinutes()).slice(-2) + ':' +
('0' + u.getSeconds()).slice(-2) + '.' +
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
},
clearConsole() {
this.consoleBuffer = "";
},
copyConsole() {
const input = document.createElement('textarea');
input.innerHTML = this.consoleBuffer;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
}
});
</script>
<style>
#console {
background-color: #0C0C0C;
color: #CCCCCC;
padding: 8px;
font-family: courier new;
font-size: .875em;
}
</style>

View File

@ -219,7 +219,7 @@ export default defineComponent({
getPinMappingList() {
this.pinMappingLoading = true;
fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((response) => handleResponse(response, this.$emitter, this.$router, true))
.then(
(data) => {
this.pinMappingList = data;
@ -246,6 +246,9 @@ export default defineComponent({
.then(
(data) => {
this.deviceConfigList = data;
if (this.deviceConfigList.curPin.name === "") {
this.deviceConfigList.curPin.name = "Default";
}
this.dataLoading = false;
}
)

View File

@ -0,0 +1,18 @@
<template>
<BasePage :title="$t('Error.Oops')">
<div class="alert alert-danger" role="alert">
<h2>{{ $route.params.message }}</h2>
</div>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
},
});
</script>

View File

@ -191,7 +191,7 @@ export default defineComponent({
const remoteHostUrl = "/api/system/status";
// Use a simple fetch request to check if the remote host is reachable
fetch(remoteHostUrl, { method: 'HEAD' })
fetch(remoteHostUrl, { method: 'GET' })
.then(response => {
// Check if the response status is OK (200-299 range)
if (response.ok) {

View File

@ -5,14 +5,20 @@
<div class="row gy-3">
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]">
<div class="nav nav-pills row-cols-sm-1" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link"
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link border border-primary text-break"
:id="'v-pills-' + inverter.serial + '-tab'" data-bs-toggle="pill"
:data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab"
aria-controls="'v-pills-' + inverter.serial" aria-selected="true">
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
{{ inverter.name }}
<div class="row">
<div class="col-auto col-sm-2">
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
</div>
<div class="col-sm-9">
{{ inverter.name }}
</div>
</div>
</button>
</div>
</div>
@ -470,17 +476,15 @@ export default defineComponent({
}
};
var self = this;
this.socket.onopen = function (event) {
this.socket.onopen = (event) => {
console.log(event);
console.log("Successfully connected to the echo websocket server...");
self.isWebsocketConnected = true;
this.isWebsocketConnected = true;
};
this.socket.onclose = function () {
this.socket.onclose = () => {
console.log("Connection to websocket closed...")
self.isWebsocketConnected = false;
this.isWebsocketConnected = false;
}
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect

View File

@ -99,7 +99,7 @@ export default defineComponent({
}
},
watch: {
allowVersionInfo(allow: Boolean) {
allowVersionInfo(allow: boolean) {
localStorage.setItem("allowVersionInfo", allow ? "1" : "0");
if (allow) {
this.getUpdateInfo();

View File

@ -12,6 +12,7 @@ import path from 'path'
// example 'vite.user.ts': export const proxy_target = '192.168.16.107'
let proxy_target;
try {
// eslint-disable-next-line
proxy_target = require('./vite.user.ts').proxy_target;
} catch (error) {
proxy_target = '192.168.20.110';

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.