This commit is contained in:
Ralf Bauer 2024-05-27 18:42:02 +02:00
commit d6c183c6d6
101 changed files with 1935 additions and 1895 deletions

View File

@ -119,7 +119,7 @@ jobs:
- name: Build Changelog - name: Build Changelog
id: github_release id: github_release
uses: mikepenz/release-changelog-builder-action@v3 uses: mikepenz/release-changelog-builder-action@v4
with: with:
failOnError: true failOnError: true
commitMode: true commitMode: true
@ -138,7 +138,7 @@ jobs:
for i in */; do cp ${i}opendtu-*.bin ./; done for i in */; do cp ${i}opendtu-*.bin ./; done
- name: Create release - name: Create release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
body: ${{steps.github_release.outputs.changelog}} body: ${{steps.github_release.outputs.changelog}}
draft: False draft: False

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,6 @@
"DavidAnson.vscode-markdownlint", "DavidAnson.vscode-markdownlint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"Vue.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"platformio.platformio-ide" "platformio.platformio-ide"
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [

View File

@ -1,4 +1,8 @@
# OpenDTU-Database-Database # OpenDTU-Database-Database
# OpenDTU-Database
One year OpenDTU-Database
![Image](docs/screenshots/Screenshot_2024-05-23_131208.png)
[![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml) [![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
[![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml) [![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
@ -94,3 +98,6 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 | | TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 | | TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 | | TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 |
| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@ -5,7 +5,7 @@
#include <cstdint> #include <cstdint>
#define CONFIG_FILENAME "/config.json" #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_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_PASSWORD_STRLEN 64
@ -30,8 +30,6 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63 #define DEV_MAX_MAPPING_NAME_STRLEN 63
#define JSON_BUFFER_SIZE 12288
struct CHANNEL_CONFIG_T { struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower; uint16_t MaxChannelPower;
char Name[CHAN_MAX_NAME_STRLEN]; char Name[CHAN_MAX_NAME_STRLEN];
@ -168,6 +166,7 @@ public:
INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id);
}; };
extern ConfigurationClass Configuration; extern ConfigurationClass Configuration;

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 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); 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 createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(DynamicJsonDocument& doc); 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 getDtuUniqueId();
static String getDtuUrl(); static String getDtuUrl();

View File

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

View File

@ -23,6 +23,7 @@
#include "WebApi_ws_console.h" #include "WebApi_ws_console.h"
#include "WebApi_ws_live.h" #include "WebApi_ws_live.h"
#include "WebApi_database.h" #include "WebApi_database.h"
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
@ -38,6 +39,10 @@ public:
static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); 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: private:
AsyncWebServer _server; AsyncWebServer _server;

View File

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

View File

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

View File

@ -0,0 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// The referenced values are generated by pio-scripts/auto_firmware_version.py
extern const char *__COMPILED_GIT_HASH__;
// extern const char *__COMPILED_DATE_TIME_UTC_STR__;

View File

@ -22,7 +22,8 @@
#define MDNS_ENABLED false #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_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
#define NTP_TIMEZONEDESCR "Europe/Berlin" #define NTP_TIMEZONEDESCR "Europe/Berlin"
#define NTP_LONGITUDE 10.4515f #define NTP_LONGITUDE 10.4515f

View File

@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
/* /*
* Copyright (C) 2022-2023 Thomas Basler and others * Copyright (C) 2022-2024 Thomas Basler and others
*/ */
#include "Hoymiles.h" #include "Hoymiles.h"
#include "Utils.h" #include "Utils.h"
#include "inverters/HERF_2CH.h"
#include "inverters/HERF_4CH.h"
#include "inverters/HMS_1CH.h" #include "inverters/HMS_1CH.h"
#include "inverters/HMS_1CHv2.h" #include "inverters/HMS_1CHv2.h"
#include "inverters/HMS_2CH.h" #include "inverters/HMS_2CH.h"
@ -112,7 +114,7 @@ void HoymilesClass::loop()
} }
// Fetch grid profile // 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(); iv->sendGridOnProFileParaRequest();
} }
@ -168,6 +170,10 @@ std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, c
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial); i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
} else if (HM_1CH::isValidSerial(serial)) { } else if (HM_1CH::isValidSerial(serial)) {
i = std::make_shared<HM_1CH>(_radioNrf.get(), serial); i = std::make_shared<HM_1CH>(_radioNrf.get(), serial);
} else if (HERF_2CH::isValidSerial(serial)) {
i = std::make_shared<HERF_2CH>(_radioNrf.get(), serial);
} else if (HERF_4CH::isValidSerial(serial)) {
i = std::make_shared<HERF_4CH>(_radioNrf.get(), serial);
} }
if (i) { if (i) {

View File

@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "TimeoutHelper.h"
#include "commands/CommandAbstract.h" #include "commands/CommandAbstract.h"
#include "types.h" #include "types.h"
#include <memory>
#include <ThreadSafeQueue.h> #include <ThreadSafeQueue.h>
#include <TimeoutHelper.h>
#include <memory>
class HoymilesRadio { class HoymilesRadio {
public: public:

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HERF_2CH.h"
static const byteAssign_t byteAssignment[] = {
{ TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 },
{ TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 },
{ TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 },
{ TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 },
{ TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 },
{ TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 },
{ TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 },
{ TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 },
{ TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 },
{ TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 },
{ TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_Q, UNIT_VAR, 32, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 },
{ TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 },
{ TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 },
{ TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 },
{ TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 },
{ TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 },
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
bool HERF_2CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x282100000000 && serial <= 0x2821ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x2821;
}
String HERF_2CH::typeName() const
{
return "HERF-800-2T";
}
const byteAssign_t* HERF_2CH::getByteAssignment() const
{
return byteAssignment;
}
uint8_t HERF_2CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "HM_Abstract.h"
class HERF_2CH : public HM_Abstract {
public:
explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial);
static bool isValidSerial(const uint64_t serial);
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HERF_4CH.h"
HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial)
: HM_4CH(radio, serial) {};
bool HERF_4CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x280100000000 && serial <= 0x2801ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x2801;
}
String HERF_4CH::typeName() const
{
return "HERF-1600/1800-4T";
}

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "HM_4CH.h"
class HERF_4CH : public HM_4CH {
public:
explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial);
static bool isValidSerial(const uint64_t serial);
String typeName() const;
};

View File

@ -33,7 +33,7 @@ HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial)
bool HMS_1CH::isValidSerial(const uint64_t serial) bool HMS_1CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x112400000000 && serial <= 0x112499999999 // serial >= 0x112400000000 && serial <= 0x1124ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1124; return preSerial == 0x1124;
} }

View File

@ -33,7 +33,7 @@ HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial)
bool HMS_1CHv2::isValidSerial(const uint64_t serial) bool HMS_1CHv2::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x112500000000 && serial <= 0x112599999999 // serial >= 0x112500000000 && serial <= 0x1125ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1125; return preSerial == 0x1125;
} }

View File

@ -40,7 +40,7 @@ HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial)
bool HMS_2CH::isValidSerial(const uint64_t serial) bool HMS_2CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x114400000000 && serial <= 0x114499999999 // serial >= 0x114400000000 && serial <= 0x1144ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1144; return preSerial == 0x1144;
} }

View File

@ -54,7 +54,7 @@ HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial)
bool HMS_4CH::isValidSerial(const uint64_t serial) bool HMS_4CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x116400000000 && serial <= 0x116499999999 // serial >= 0x116400000000 && serial <= 0x1164ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1164; return preSerial == 0x1164;
} }

View File

@ -63,14 +63,14 @@ HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial)
bool HMT_4CH::isValidSerial(const uint64_t serial) bool HMT_4CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x136100000000 && serial <= 0x136199999999 // serial >= 0x136100000000 && serial <= 0x1361ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1361; return preSerial == 0x1361;
} }
String HMT_4CH::typeName() const 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 const byteAssign_t* HMT_4CH::getByteAssignment() const

View File

@ -77,14 +77,14 @@ HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial)
bool HMT_6CH::isValidSerial(const uint64_t serial) bool HMT_6CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x138200000000 && serial <= 0x138299999999 // serial >= 0x138200000000 && serial <= 0x1382ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff; uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1382; return preSerial == 0x1382;
} }
String HMT_6CH::typeName() const String HMT_6CH::typeName() const
{ {
return F("HMT-1800/2250-6T"); return "HMT-1800/2250-6T";
} }
const byteAssign_t* HMT_6CH::getByteAssignment() const const byteAssign_t* HMT_6CH::getByteAssignment() const

View File

@ -33,7 +33,7 @@ HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial)
bool HM_1CH::isValidSerial(const uint64_t serial) bool HM_1CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x112100000000 && serial <= 0x112199999999 // serial >= 0x112100000000 && serial <= 0x1121ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = (uint8_t)(serial >> 40);

View File

@ -41,7 +41,7 @@ HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial)
bool HM_2CH::isValidSerial(const uint64_t serial) bool HM_2CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x114100000000 && serial <= 0x114199999999 // serial >= 0x114100000000 && serial <= 0x1141ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = (uint8_t)(serial >> 40);

View File

@ -54,7 +54,7 @@ HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial)
bool HM_4CH::isValidSerial(const uint64_t serial) bool HM_4CH::isValidSerial(const uint64_t serial)
{ {
// serial >= 0x116100000000 && serial <= 0x116199999999 // serial >= 0x116100000000 && serial <= 0x1161ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = (uint8_t)(serial >> 40);

View File

@ -11,3 +11,5 @@
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 | | HMT_6CH | HMT-1800/2250-6T | 1382 |
| HERF_2CH | HERF 800 | 2821 |
| HERF_4CH | HERF 1800 | 2801 |

View File

@ -55,11 +55,12 @@ const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> AlarmLogParser::_alarmMe
{ AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" }, { AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" },
{ AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" }, { AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" },
{ AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" }, { AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" },
{ AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Eletrizitätsnetzausfall", "Réseau: Panne du réseau électrique" }, { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Elektrizitätsnetzausfall", "Réseau: Panne du réseau électrique" },
{ AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" }, { AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" },
{ AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection dîlots" }, { AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection dîlots" },
{ AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" }, { AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" },
{ AlarmMessageType_t::ALL, 152, "Grid: Phase angle difference between two phases exceeded 5° >10 times", "", "" },
{ AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" }, { AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" },
{ AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" }, { AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" },
{ AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" }, { AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" },

View File

@ -8,7 +8,7 @@
#define ALARM_LOG_ENTRY_SIZE 12 #define ALARM_LOG_ENTRY_SIZE 12
#define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4) #define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4)
#define ALARM_MSG_COUNT 130 #define ALARM_MSG_COUNT 131
struct AlarmLogEntry_t { struct AlarmLogEntry_t {
uint16_t MessageId; uint16_t MessageId;

View File

@ -52,7 +52,11 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0 { { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0
{ { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01
{ { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01 { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01
{ { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00
{ { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00
{ { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00
}; };
DevInfoParser::DevInfoParser() DevInfoParser::DevInfoParser()
@ -200,7 +204,7 @@ bool DevInfoParser::containsValidData() const
struct tm info; struct tm info;
localtime_r(&t, &info); localtime_r(&t, &info);
return info.tm_year > (2016 - 1900); return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097;
} }
uint8_t DevInfoParser::getDevIdx() const uint8_t DevInfoParser::getDevIdx() const

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later // 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 "GridProfileParser.h"
#include "../Hoymiles.h" #include "../Hoymiles.h"
@ -446,6 +446,11 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
return l; 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 GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version)
{ {
uint8_t count = 0; uint8_t count = 0;

View File

@ -43,6 +43,8 @@ public:
std::list<GridProfileSection_t> getProfile() const; std::list<GridProfileSection_t> getProfile() const;
bool containsValidData() const;
private: private:
static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); 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); static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version);

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

@ -2,6 +2,7 @@
# #
# Copyright (C) 2022 Thomas Basler and others # Copyright (C) 2022 Thomas Basler and others
# #
import os
import pkg_resources import pkg_resources
Import("env") Import("env")
@ -15,15 +16,64 @@ if missing_pkgs:
from dulwich import porcelain from dulwich import porcelain
def get_firmware_specifier_build_flag():
def updateFileIfChanged(filename, content):
mustUpdate = True
try:
fp = open(filename, "rb")
if fp.read() == content:
mustUpdate = False
fp.close()
except:
pass
if mustUpdate:
fp = open(filename, "wb")
fp.write(content)
fp.close()
return mustUpdate
def get_build_version():
try: try:
build_version = porcelain.describe('.') # '.' refers to the repository root dir build_version = porcelain.describe('.') # '.' refers to the repository root dir
except: except:
build_version = "g0000000" build_version = "g0000000"
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
print ("Firmware Revision: " + build_version) print ("Firmware Revision: " + build_version)
return build_version
def get_firmware_specifier_build_flag():
build_version = get_build_version()
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
return (build_flag) return (build_flag)
env.Append(
def do_main():
if 0:
# this results in a full recompilation of the whole project after each commit
env.Append(
BUILD_FLAGS=[get_firmware_specifier_build_flag()] BUILD_FLAGS=[get_firmware_specifier_build_flag()]
) )
else:
# we just create a .c file containing the needed datas
targetfile = os.path.join(env.subst("$BUILD_DIR"), "__compiled_constants.c")
lines = ""
lines += "/* Generated file within build process - Do NOT edit */\n"
if 0:
# Add the current date and time as string in UTC timezone
from datetime import datetime, timezone
now = datetime.now(tz=timezone.utc)
COMPILED_DATE_TIME_UTC_STR = now.strftime("%Y/%m/%d %H:%M:%S")
lines += 'const char *__COMPILED_DATE_TIME_UTC_STR__ = "%s";\n' % (COMPILED_DATE_TIME_UTC_STR)
if 1:
# Add the description of the current git revision
lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version())
updateFileIfChanged(targetfile, bytes(lines, "utf-8"))
# Add the created file to the buildfiles - platformio knows how to handle *.c files
env.AppendUnique(PIOBUILDFILES=[targetfile])
do_main()

View File

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

View File

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

View File

@ -29,11 +29,16 @@ const uint8_t languages[] = {
}; };
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" }; static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" };
static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" }; static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" };
static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" };
static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" }; static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" };
static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" }; static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" };
static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" }; static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" };
DisplayGraphicClass::DisplayGraphicClass() DisplayGraphicClass::DisplayGraphicClass()
@ -129,6 +134,10 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line)
offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens
dispX += offset; dispX += offset;
} }
if (dispX > _display->getDisplayWidth()) {
dispX = 0;
}
_display->drawStr(dispX, _lineOffsets[line], text); _display->drawStr(dispX, _lineOffsets[line], text);
} }
@ -237,15 +246,20 @@ void DisplayGraphicClass::loop()
//<======================= //<=======================
if (showText) { if (showText) {
//=====> Today & Total Production ======= // Daily production
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); float wattsToday = Datastore.getTotalAcYieldDayEnabled();
if (wattsToday >= 10000) {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000);
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday);
}
printText(_fmtText, 1); printText(_fmtText, 1);
const float watts = Datastore.getTotalAcYieldTotalEnabled(); // Total production
auto const format = (watts >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], watts); auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal);
printText(_fmtText, 2); printText(_fmtText, 2);
//<=======================
//=====> IP or Date-Time ======== //=====> IP or Date-Time ========
// Change every 3 seconds // Change every 3 seconds

View File

@ -51,9 +51,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
if (PinMapping.isValidCmt2300Config()) { if (PinMapping.isValidCmt2300Config()) {
Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); 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)); 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); Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
} }

View File

@ -8,6 +8,7 @@
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "Utils.h" #include "Utils.h"
#include "defaults.h" #include "defaults.h"
#include "__compiled_constants.h"
MqttHandleHassClass MqttHandleHass; MqttHandleHassClass MqttHandleHass;
@ -137,10 +138,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
name = "CH" + chanNum + " " + fieldName; name = "CH" + chanNum + " " + fieldName;
} }
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["stat_t"] = stateTopic; root["stat_t"] = stateTopic;
@ -163,6 +161,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
root["stat_cla"] = stateCls; root["stat_cla"] = stateCls;
} }
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
serializeJson(root, buffer); serializeJson(root, buffer);
publish(configTopic, buffer); publish(configTopic, buffer);
@ -185,10 +187,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
@ -204,6 +203,10 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
createInverterInfo(root, inv); createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
serializeJson(root, buffer); serializeJson(root, buffer);
publish(configTopic, buffer); publish(configTopic, buffer);
@ -227,10 +230,7 @@ void MqttHandleHassClass::publishInverterNumber(
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic; const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic;
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
@ -246,6 +246,10 @@ void MqttHandleHassClass::publishInverterNumber(
createInverterInfo(root, inv); createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
serializeJson(root, buffer); serializeJson(root, buffer);
publish(configTopic, buffer); publish(configTopic, buffer);
@ -265,10 +269,7 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = caption; root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId; root["uniq_id"] = serial + "_" + sensorId;
@ -278,6 +279,10 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
createInverterInfo(root, inv); createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
serializeJson(root, buffer); serializeJson(root, buffer);
publish(configTopic, buffer); publish(configTopic, buffer);
@ -293,10 +298,7 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
topic = id; topic = id;
} }
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id; root["uniq_id"] = getDtuUniqueId() + "_" + id;
@ -322,6 +324,10 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
createDtuInfo(root); createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config"; const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer); serializeJson(root, buffer);
@ -339,10 +345,7 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
topic = String("dtu/") + "/" + id; topic = String("dtu/") + "/" + id;
} }
DynamicJsonDocument root(1024); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
root["name"] = name; root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id; root["uniq_id"] = getDtuUniqueId() + "_" + id;
@ -359,13 +362,17 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
createDtuInfo(root); createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer; String buffer;
const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config"; const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer); serializeJson(root, buffer);
publish(configTopic, 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( createDeviceInfo(
root, root,
@ -374,11 +381,11 @@ void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::sha
getDtuUrl(), getDtuUrl(),
"OpenDTU", "OpenDTU",
inv->typeName(), inv->typeName(),
AUTO_GIT_HASH, __COMPILED_GIT_HASH__,
getDtuUniqueId()); getDtuUniqueId());
} }
void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) void MqttHandleHassClass::createDtuInfo(JsonDocument& root)
{ {
createDeviceInfo( createDeviceInfo(
root, root,
@ -387,16 +394,16 @@ void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root)
getDtuUrl(), getDtuUrl(),
"OpenDTU", "OpenDTU",
"OpenDTU", "OpenDTU",
AUTO_GIT_HASH); __COMPILED_GIT_HASH__);
} }
void MqttHandleHassClass::createDeviceInfo( void MqttHandleHassClass::createDeviceInfo(
DynamicJsonDocument& root, JsonDocument& root,
const String& name, const String& identifiers, const String& configuration_url, const String& name, const String& identifiers, const String& configuration_url,
const String& manufacturer, const String& model, const String& sw_version, const String& manufacturer, const String& model, const String& sw_version,
const String& via_device) const String& via_device)
{ {
auto object = root.createNestedObject("dev"); auto object = root["dev"].to<JsonObject>();
object["name"] = name; object["name"] = name;
object["ids"] = identifiers; object["ids"] = identifiers;

View File

@ -10,6 +10,7 @@
#include "defaults.h" #include "defaults.h"
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <ETH.h> #include <ETH.h>
#include "__compiled_constants.h"
NetworkSettingsClass::NetworkSettingsClass() NetworkSettingsClass::NetworkSettingsClass()
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this)) : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this))
@ -136,7 +137,7 @@ void NetworkSettingsClass::handleMDNS()
MDNS.addService("http", "tcp", 80); MDNS.addService("http", "tcp", 80);
MDNS.addService("opendtu", "tcp", 80); MDNS.addService("opendtu", "tcp", 80);
MDNS.addServiceTxt("opendtu", "tcp", "git_hash", AUTO_GIT_HASH); MDNS.addServiceTxt("opendtu", "tcp", "git_hash", __COMPILED_GIT_HASH__);
MessageOutput.println("done"); MessageOutput.println("done");
} else { } else {

View File

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

View File

@ -69,9 +69,9 @@ void Utils::restartDtu()
ESP.restart(); 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); MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line);
return false; return false;
} }

View File

@ -4,6 +4,7 @@
*/ */
#include "WebApi.h" #include "WebApi.h"
#include "Configuration.h" #include "Configuration.h"
#include "MessageOutput.h"
#include "defaults.h" #include "defaults.h"
#include <AsyncJson.h> #include <AsyncJson.h>
@ -86,4 +87,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; WebApiClass WebApi;

View File

@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
requestFile = name; requestFile = name;
} else { } else {
request->send(404); request->send(404);
return;
} }
} }
@ -53,51 +54,24 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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"))) { if (!(root.containsKey("delete"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["delete"].as<bool>() == false) { if (root["delete"].as<bool>() == false) {
retMsg["message"] = "Not deleted anything!"; retMsg["message"] = "Not deleted anything!";
retMsg["code"] = WebApiError::ConfigNotDeleted; retMsg["code"] = WebApiError::ConfigNotDeleted;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -105,8 +79,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
retMsg["message"] = "Configuration resettet. Rebooting now..."; retMsg["message"] = "Configuration resettet. Rebooting now...";
retMsg["code"] = WebApiError::ConfigSuccess; retMsg["code"] = WebApiError::ConfigSuccess;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
Utils::removeAllFiles(); Utils::removeAllFiles();
Utils::restartDtu(); Utils::restartDtu();
@ -120,7 +93,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
auto data = root.createNestedArray("configs"); auto data = root["configs"].to<JsonArray>();
File rootfs = LittleFS.open("/"); File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile(); File file = rootfs.openNextFile();
@ -128,15 +101,14 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
if (file.isDirectory()) { if (file.isDirectory()) {
continue; continue;
} }
JsonObject obj = data.createNestedObject(); JsonObject obj = data.add<JsonObject>();
obj["name"] = String(file.name()); obj["name"] = String(file.name());
file = rootfs.openNextFile(); file = rootfs.openNextFile();
} }
file.close(); file.close();
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)

View File

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

View File

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

View File

@ -62,10 +62,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
root["cmt_country"] = config.Dtu.Cmt.CountryMode; root["cmt_country"] = config.Dtu.Cmt.CountryMode;
root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth(); 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(); auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList();
for (const auto& definition : countryDefs) { for (const auto& definition : countryDefs) {
auto obj = data.createNestedObject(); auto obj = data.add<JsonObject>();
obj["freq_default"] = definition.definition.Freq_Default; obj["freq_default"] = definition.definition.Freq_Default;
obj["freq_min"] = definition.definition.Freq_Min; obj["freq_min"] = definition.definition.Freq_Min;
obj["freq_max"] = definition.definition.Freq_Max; 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; obj["freq_legal_max"] = definition.definition.Freq_Legal_Max;
} }
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
@ -84,37 +83,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("serial")
&& root.containsKey("pollinterval") && root.containsKey("pollinterval")
@ -124,48 +98,45 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("cmt_country"))) { && root.containsKey("cmt_country"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["serial"].as<uint64_t>() == 0) { // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) {
retMsg["message"] = "Serial cannot be zero!"; retMsg["message"] = "Serial cannot be zero!";
retMsg["code"] = WebApiError::DtuSerialZero; retMsg["code"] = WebApiError::DtuSerialZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["pollinterval"].as<uint32_t>() == 0) { if (root["pollinterval"].as<uint32_t>() == 0) {
retMsg["message"] = "Poll interval must be greater zero!"; retMsg["message"] = "Poll interval must be greater zero!";
retMsg["code"] = WebApiError::DtuPollZero; retMsg["code"] = WebApiError::DtuPollZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["nrf_palevel"].as<uint8_t>() > 3) { if (root["nrf_palevel"].as<uint8_t>() > 3) {
retMsg["message"] = "Invalid power level setting!"; retMsg["message"] = "Invalid power level setting!";
retMsg["code"] = WebApiError::DtuInvalidPowerLevel; retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["cmt_palevel"].as<int8_t>() < -10 || root["cmt_palevel"].as<int8_t>() > 20) { if (root["cmt_palevel"].as<int8_t>() < -10 || root["cmt_palevel"].as<int8_t>() > 20) {
retMsg["message"] = "Invalid power level setting!"; retMsg["message"] = "Invalid power level setting!";
retMsg["code"] = WebApiError::DtuInvalidPowerLevel; retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["cmt_country"].as<uint8_t>() >= CountryModeId_t::CountryModeId_Max) { if (root["cmt_country"].as<uint8_t>() >= CountryModeId_t::CountryModeId_Max) {
retMsg["message"] = "Invalid country setting!"; retMsg["message"] = "Invalid country setting!";
retMsg["code"] = WebApiError::DtuInvalidCmtCountry; retMsg["code"] = WebApiError::DtuInvalidCmtCountry;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -178,15 +149,13 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::DtuInvalidCmtFrequency; retMsg["code"] = WebApiError::DtuInvalidCmtFrequency;
retMsg["param"]["min"] = FrequencyDefinition.Freq_Min; retMsg["param"]["min"] = FrequencyDefinition.Freq_Min;
retMsg["param"]["max"] = FrequencyDefinition.Freq_Max; retMsg["param"]["max"] = FrequencyDefinition.Freq_Max;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
// Interpret the string as a hex value and convert it to uint64_t config.Dtu.Serial = serial;
config.Dtu.Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>(); config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>(); config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>(); config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
@ -195,8 +164,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
_applyDataTask.enable(); _applyDataTask.enable();
_applyDataTask.restart();
} }

View File

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

View File

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

View File

@ -29,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
return; return;
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
JsonArray data = root.createNestedArray("inverter"); JsonArray data = root["inverter"].to<JsonArray>();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) { if (config.Inverter[i].Serial > 0) {
JsonObject obj = data.createNestedObject(); JsonObject obj = data.add<JsonObject>();
obj["id"] = i; obj["id"] = i;
obj["name"] = String(config.Inverter[i].Name); obj["name"] = String(config.Inverter[i].Name);
obj["order"] = config.Inverter[i].Order; obj["order"] = config.Inverter[i].Order;
@ -67,9 +67,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size(); 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++) { 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["name"] = config.Inverter[i].channel[c].Name;
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
@ -77,8 +77,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
} }
} }
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
@ -88,52 +87,28 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("serial")
&& root.containsKey("name"))) { && root.containsKey("name"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["serial"].as<uint64_t>() == 0) { // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!"; retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero; retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -141,8 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::InverterNameLength; retMsg["code"] = WebApiError::InverterNameLength;
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -152,20 +126,18 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!"; retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!";
retMsg["code"] = WebApiError::InverterCount; retMsg["code"] = WebApiError::InverterCount;
retMsg["param"]["max"] = INV_MAX_COUNT; retMsg["param"]["max"] = INV_MAX_COUNT;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
// Interpret the string as a hex value and convert it to uint64_t // Interpret the string as a hex value and convert it to uint64_t
inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16); inverter->Serial = serial;
strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN); strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!"); WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!");
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial); auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial);
@ -185,59 +157,34 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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"))) { if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) { if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
retMsg["message"] = "Invalid ID specified!"; retMsg["message"] = "Invalid ID specified!";
retMsg["code"] = WebApiError::InverterInvalidId; retMsg["code"] = WebApiError::InverterInvalidId;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["serial"].as<uint64_t>() == 0) { // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!"; retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero; retMsg["code"] = WebApiError::InverterSerialZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -245,8 +192,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::InverterNameLength; retMsg["code"] = WebApiError::InverterNameLength;
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -254,14 +200,13 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) {
retMsg["message"] = "Invalid amount of max channel setting given!"; retMsg["message"] = "Invalid amount of max channel setting given!";
retMsg["code"] = WebApiError::InverterInvalidMaxChannel; retMsg["code"] = WebApiError::InverterInvalidMaxChannel;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()]; INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];
uint64_t new_serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16); uint64_t new_serial = serial;
uint64_t old_serial = inverter.Serial; uint64_t old_serial = inverter.Serial;
// Interpret the string as a hex value and convert it to uint64_t // Interpret the string as a hex value and convert it to uint64_t
@ -287,8 +232,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!");
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial); std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial);
@ -327,51 +271,24 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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"))) { if (!(root.containsKey("id"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) { if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
retMsg["message"] = "Invalid ID specified!"; retMsg["message"] = "Invalid ID specified!";
retMsg["code"] = WebApiError::InverterInvalidId; retMsg["code"] = WebApiError::InverterInvalidId;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -380,13 +297,11 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
Hoymiles.removeInverterBySerial(inverter.Serial); Hoymiles.removeInverterBySerial(inverter.Serial);
inverter.Serial = 0; Configuration.deleteInverterById(inverter_id);
strncpy(inverter.Name, "", sizeof(inverter.Name));
WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!"); WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();
} }
@ -398,43 +313,17 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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"))) { if (!(root.containsKey("order"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -452,6 +341,5 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -47,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
root[serial]["limit_set_status"] = limitStatus; root[serial]["limit_set_status"] = limitStatus;
} }
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
@ -58,53 +57,29 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("serial")
&& root.containsKey("limit_value") && root.containsKey("limit_value")
&& root.containsKey("limit_type"))) { && root.containsKey("limit_type"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["serial"].as<uint64_t>() == 0) { // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!"; retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::LimitSerialZero; retMsg["code"] = WebApiError::LimitSerialZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -112,8 +87,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!";
retMsg["code"] = WebApiError::LimitInvalidLimit; retMsg["code"] = WebApiError::LimitInvalidLimit;
retMsg["param"]["max"] = MAX_INVERTER_LIMIT; retMsg["param"]["max"] = MAX_INVERTER_LIMIT;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -124,12 +98,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Invalid type specified!"; retMsg["message"] = "Invalid type specified!";
retMsg["code"] = WebApiError::LimitInvalidType; retMsg["code"] = WebApiError::LimitInvalidType;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
uint16_t limit = root["limit_value"].as<uint16_t>(); uint16_t limit = root["limit_value"].as<uint16_t>();
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>(); PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
@ -137,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
if (inv == nullptr) { if (inv == nullptr) {
retMsg["message"] = "Invalid inverter specified!"; retMsg["message"] = "Invalid inverter specified!";
retMsg["code"] = WebApiError::LimitInvalidInverter; retMsg["code"] = WebApiError::LimitInvalidInverter;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -148,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
retMsg["message"] = "Settings saved!"; retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess; retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -22,44 +22,18 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
return; 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(); 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"))) { if (!(root.containsKey("reboot"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -68,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
retMsg["message"] = "Reboot triggered!"; retMsg["message"] = "Reboot triggered!";
retMsg["code"] = WebApiError::MaintenanceRebootTriggered; retMsg["code"] = WebApiError::MaintenanceRebootTriggered;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
Utils::restartDtu(); Utils::restartDtu();
} else { } else {
retMsg["message"] = "Reboot cancled!"; retMsg["message"] = "Reboot cancled!";
retMsg["code"] = WebApiError::MaintenanceRebootCancled; retMsg["code"] = WebApiError::MaintenanceRebootCancled;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
} }

View File

@ -26,7 +26,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
return; return;
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); 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_topic"] = config.Mqtt.Hass.Topic;
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
@ -60,7 +59,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
return; return;
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get(); 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_topic"] = config.Mqtt.Hass.Topic;
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
@ -98,38 +96,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
return; 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(); 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") if (!(root.containsKey("mqtt_enabled")
&& root.containsKey("mqtt_hostname") && root.containsKey("mqtt_hostname")
@ -155,8 +128,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("mqtt_hass_individualpanels"))) { && root.containsKey("mqtt_hass_individualpanels"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!";
retMsg["code"] = WebApiError::MqttHostnameLength; retMsg["code"] = WebApiError::MqttHostnameLength;
retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN; retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttUsernameLength; retMsg["code"] = WebApiError::MqttUsernameLength;
retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN; retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_password"].as<String>().length() > MQTT_MAX_PASSWORD_STRLEN) { 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["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttPasswordLength; retMsg["code"] = WebApiError::MqttPasswordLength;
retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN; retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) { 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["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttTopicLength; retMsg["code"] = WebApiError::MqttTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_topic"].as<String>().indexOf(' ') != -1) { if (root["mqtt_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "Topic must not contain space characters!"; retMsg["message"] = "Topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttTopicCharacter; retMsg["code"] = WebApiError::MqttTopicCharacter;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (!root["mqtt_topic"].as<String>().endsWith("/")) { if (!root["mqtt_topic"].as<String>().endsWith("/")) {
retMsg["message"] = "Topic must end with a slash (/)!"; retMsg["message"] = "Topic must end with a slash (/)!";
retMsg["code"] = WebApiError::MqttTopicTrailingSlash; retMsg["code"] = WebApiError::MqttTopicTrailingSlash;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_port"].as<uint>() == 0 || root["mqtt_port"].as<uint>() > 65535) { 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["message"] = "Port must be a number between 1 and 65535!";
retMsg["code"] = WebApiError::MqttPort; retMsg["code"] = WebApiError::MqttPort;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttCertificateLength; retMsg["code"] = WebApiError::MqttCertificateLength;
retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN; retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtTopicLength; retMsg["code"] = WebApiError::MqttLwtTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_lwt_topic"].as<String>().indexOf(' ') != -1) { if (root["mqtt_lwt_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "LWT topic must not contain space characters!"; retMsg["message"] = "LWT topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttLwtTopicCharacter; retMsg["code"] = WebApiError::MqttLwtTopicCharacter;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtOnlineLength; retMsg["code"] = WebApiError::MqttLwtOnlineLength;
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttLwtOfflineLength; retMsg["code"] = WebApiError::MqttLwtOfflineLength;
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -269,8 +229,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!"; retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!";
retMsg["code"] = WebApiError::MqttLwtQos; retMsg["code"] = WebApiError::MqttLwtQos;
retMsg["param"]["max"] = 2; retMsg["param"]["max"] = 2;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -279,8 +238,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::MqttPublishInterval; retMsg["code"] = WebApiError::MqttPublishInterval;
retMsg["param"]["min"] = 5; retMsg["param"]["min"] = 5;
retMsg["param"]["max"] = 65535; retMsg["param"]["max"] = 65535;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
retMsg["code"] = WebApiError::MqttHassTopicLength; retMsg["code"] = WebApiError::MqttHassTopicLength;
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["mqtt_hass_topic"].as<String>().indexOf(' ') != -1) { if (root["mqtt_hass_topic"].as<String>().indexOf(' ') != -1) {
retMsg["message"] = "Hass topic must not contain space characters!"; retMsg["message"] = "Hass topic must not contain space characters!";
retMsg["code"] = WebApiError::MqttHassTopicCharacter; retMsg["code"] = WebApiError::MqttHassTopicCharacter;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
} }
@ -331,8 +287,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
MqttSettings.performReconnect(); MqttSettings.performReconnect();
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();

View File

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

View File

@ -63,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable(); root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable();
root["sun_isDayPeriod"] = SunPosition.isDayPeriod(); root["sun_isDayPeriod"] = SunPosition.isDayPeriod();
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
@ -84,8 +83,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
root["latitude"] = config.Ntp.Latitude; root["latitude"] = config.Ntp.Latitude;
root["sunsettype"] = config.Ntp.SunsetType; root["sunsettype"] = config.Ntp.SunsetType;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
@ -95,37 +93,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("ntp_server")
&& root.containsKey("ntp_timezone") && root.containsKey("ntp_timezone")
@ -134,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
&& root.containsKey("sunsettype"))) { && root.containsKey("sunsettype"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpServerLength; retMsg["code"] = WebApiError::NtpServerLength;
retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN; retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpTimezoneLength; retMsg["code"] = WebApiError::NtpTimezoneLength;
retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN; retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!";
retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength; retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength;
retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN; retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -176,8 +145,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
NtpSettings.setServer(); NtpSettings.setServer();
NtpSettings.setTimezone(); NtpSettings.setTimezone();
@ -208,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
root["minute"] = timeinfo.tm_min; root["minute"] = timeinfo.tm_min;
root["second"] = timeinfo.tm_sec; root["second"] = timeinfo.tm_sec;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
@ -219,37 +186,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("year")
&& root.containsKey("month") && root.containsKey("month")
@ -259,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
&& root.containsKey("second"))) { && root.containsKey("second"))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -269,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpYearInvalid; retMsg["code"] = WebApiError::NtpYearInvalid;
retMsg["param"]["min"] = 2022; retMsg["param"]["min"] = 2022;
retMsg["param"]["max"] = 2100; retMsg["param"]["max"] = 2100;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -279,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpMonthInvalid; retMsg["code"] = WebApiError::NtpMonthInvalid;
retMsg["param"]["min"] = 1; retMsg["param"]["min"] = 1;
retMsg["param"]["max"] = 12; retMsg["param"]["max"] = 12;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -289,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpDayInvalid; retMsg["code"] = WebApiError::NtpDayInvalid;
retMsg["param"]["min"] = 1; retMsg["param"]["min"] = 1;
retMsg["param"]["max"] = 31; retMsg["param"]["max"] = 31;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -299,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpHourInvalid; retMsg["code"] = WebApiError::NtpHourInvalid;
retMsg["param"]["min"] = 0; retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 23; retMsg["param"]["max"] = 23;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -309,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpMinuteInvalid; retMsg["code"] = WebApiError::NtpMinuteInvalid;
retMsg["param"]["min"] = 0; retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 59; retMsg["param"]["max"] = 59;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -319,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::NtpSecondInvalid; retMsg["code"] = WebApiError::NtpSecondInvalid;
retMsg["param"]["min"] = 0; retMsg["param"]["min"] = 0;
retMsg["param"]["max"] = 59; retMsg["param"]["max"] = 59;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -341,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
retMsg["message"] = "Time updated!"; retMsg["message"] = "Time updated!";
retMsg["code"] = WebApiError::NtpTimeUpdated; retMsg["code"] = WebApiError::NtpTimeUpdated;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -40,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
root[inv->serialString()]["power_set_status"] = limitStatus; root[inv->serialString()]["power_set_status"] = limitStatus;
} }
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
@ -51,63 +50,37 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!(root.containsKey("serial")
&& (root.containsKey("power") && (root.containsKey("power")
|| root.containsKey("restart")))) { || root.containsKey("restart")))) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
if (root["serial"].as<uint64_t>() == 0) { // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) {
retMsg["message"] = "Serial must be a number > 0!"; retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::PowerSerialZero; retMsg["code"] = WebApiError::PowerSerialZero;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
auto inv = Hoymiles.getInverterBySerial(serial); auto inv = Hoymiles.getInverterBySerial(serial);
if (inv == nullptr) { if (inv == nullptr) {
retMsg["message"] = "Invalid inverter specified!"; retMsg["message"] = "Invalid inverter specified!";
retMsg["code"] = WebApiError::PowerInvalidInverter; retMsg["code"] = WebApiError::PowerInvalidInverter;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -124,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
retMsg["message"] = "Settings saved!"; retMsg["message"] = "Settings saved!";
retMsg["code"] = WebApiError::GenericSuccess; retMsg["code"] = WebApiError::GenericSuccess;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -9,6 +9,7 @@
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "WebApi.h" #include "WebApi.h"
#include <Hoymiles.h> #include <Hoymiles.h>
#include "__compiled_constants.h"
void WebApiPrometheusClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiPrometheusClass::init(AsyncWebServer& server, Scheduler& scheduler)
{ {
@ -29,7 +30,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_build Build info\n"); stream->print("# HELP opendtu_build Build info\n");
stream->print("# TYPE opendtu_build gauge\n"); stream->print("# TYPE opendtu_build gauge\n");
stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n",
NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); NetworkSettings.getHostname().c_str(), __COMPILED_GIT_HASH__, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
stream->print("# HELP opendtu_platform Platform info\n"); stream->print("# HELP opendtu_platform Platform info\n");
stream->print("# TYPE opendtu_platform gauge\n"); stream->print("# TYPE opendtu_platform gauge\n");
@ -142,7 +143,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
return; return;
} }
const CONFIG_T& config = Configuration.get(); const auto& config = Configuration.getInverterConfig(inv->serial());
const bool printHelp = (idx == 0 && channel == 0); const bool printHelp = (idx == 0 && channel == 0);
if (printHelp) { if (printHelp) {
@ -154,7 +155,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
idx, idx,
inv->name(), inv->name(),
channel, channel,
config.Inverter[idx].channel[channel].Name); config->channel[channel].Name);
if (printHelp) { if (printHelp) {
stream->print("# HELP opendtu_MaxPower panel maximum output power\n"); stream->print("# HELP opendtu_MaxPower panel maximum output power\n");
@ -165,7 +166,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
idx, idx,
inv->name(), inv->name(),
channel, channel,
config.Inverter[idx].channel[channel].MaxChannelPower); config->channel[channel].MaxChannelPower);
if (printHelp) { if (printHelp) {
stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"); stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n");
@ -176,5 +177,5 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
idx, idx,
inv->name(), inv->name(),
channel, channel,
config.Inverter[idx].channel[channel].YieldTotalOffset); config->channel[channel].YieldTotalOffset);
} }

View File

@ -31,8 +31,7 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request)
root["password"] = config.Security.Password; root["password"] = config.Security.Password;
root["allow_readonly"] = config.Security.AllowReadonly; root["allow_readonly"] = config.Security.AllowReadonly;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
@ -42,44 +41,18 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot(); 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") if (!root.containsKey("password")
&& root.containsKey("allow_readonly")) { && root.containsKey("allow_readonly")) {
retMsg["message"] = "Values are missing!"; retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing; retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; 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["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
retMsg["code"] = WebApiError::SecurityPasswordLength; retMsg["code"] = WebApiError::SecurityPasswordLength;
retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN; retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
return; return;
} }
@ -98,8 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
@ -114,6 +85,5 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
retMsg["message"] = "Authentication successful!"; retMsg["message"] = "Authentication successful!";
retMsg["code"] = WebApiError::SecurityAuthSuccess; retMsg["code"] = WebApiError::SecurityAuthSuccess;
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -11,10 +11,7 @@
#include <Hoymiles.h> #include <Hoymiles.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <ResetReason.h> #include <ResetReason.h>
#include "__compiled_constants.h"
#ifndef AUTO_GIT_HASH
#define AUTO_GIT_HASH ""
#endif
void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler)
{ {
@ -64,7 +61,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
char version[16]; char version[16];
snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
root["config_version"] = version; root["config_version"] = version;
root["git_hash"] = AUTO_GIT_HASH; root["git_hash"] = __COMPILED_GIT_HASH__;
root["pioenv"] = PIOENV; root["pioenv"] = PIOENV;
root["uptime"] = esp_timer_get_time() / 1000000; root["uptime"] = esp_timer_get_time() / 1000000;
@ -76,6 +73,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root["cmt_configured"] = PinMapping.isValidCmt2300Config(); root["cmt_configured"] = PinMapping.isValidCmt2300Config();
root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected();
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} }

View File

@ -73,19 +73,20 @@ void WebApiWsLiveClass::sendDataTaskCb()
try { try {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
DynamicJsonDocument root(4096); JsonDocument root;
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
continue;
}
JsonVariant var = root; JsonVariant var = root;
auto invArray = var.createNestedArray("inverters"); auto invArray = var["inverters"].to<JsonArray>();
auto invObject = invArray.createNestedObject(); auto invObject = invArray.add<JsonObject>();
generateCommonJsonResponse(var); generateCommonJsonResponse(var);
generateInverterCommonJsonResponse(invObject, inv); generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
continue;
}
String buffer; String buffer;
serializeJson(root, buffer); serializeJson(root, buffer);
@ -101,12 +102,12 @@ void WebApiWsLiveClass::sendDataTaskCb()
void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) 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, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits());
addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits());
addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits()); addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits());
JsonObject hintObj = root.createNestedObject("hints"); JsonObject hintObj = root["hints"].to<JsonObject>();
struct tm timeinfo; struct tm timeinfo;
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); 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 // Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) { 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)) { for (auto& c : inv->Statistics()->getChannelsByType(t)) {
if (t == TYPE_DC) { if (t == TYPE_DC) {
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name; chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
@ -221,21 +222,15 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
try { try {
std::lock_guard<std::mutex> lock(_mutex); std::lock_guard<std::mutex> lock(_mutex);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
auto invArray = root["inverters"].to<JsonArray>();
JsonArray invArray = root.createNestedArray("inverters"); auto serial = WebApi.parseSerialFromRequest(request);
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
if (serial > 0) { if (serial > 0) {
auto inv = Hoymiles.getInverterBySerial(serial); auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) { if (inv != nullptr) {
JsonObject invObject = invArray.createNestedObject(); JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv); generateInverterCommonJsonResponse(invObject, inv);
generateInverterChannelJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv);
} }
@ -247,15 +242,14 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
continue; continue;
} }
JsonObject invObject = invArray.createNestedObject(); JsonObject invObject = invArray.add<JsonObject>();
generateInverterCommonJsonResponse(invObject, inv); generateInverterCommonJsonResponse(invObject, inv);
} }
} }
generateCommonJsonResponse(root); generateCommonJsonResponse(root);
response->setLength(); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
request->send(response);
} catch (const std::bad_alloc& bad_alloc) { } 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()); 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,45 +9,44 @@
"preview": "vite preview --port 4173", "preview": "vite preview --port 4173",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --noEmit", "type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.3",
"bootstrap-icons-vue": "^1.11.3", "bootstrap-icons-vue": "^1.11.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pure-vue-chart": "^0.4.0", "pure-vue-chart": "^0.4.0",
"sortablejs": "^1.15.2", "sortablejs": "^1.15.2",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"vue": "^3.4.19", "vue": "^3.4.26",
"vue-google-charts": "^1.1.0", "vue-google-charts": "^1.1.0",
"vue-i18n": "^9.9.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.2.5", "vue-router": "^4.3.2",
"vue3-calendar-heatmap": "^2.0.5" "vue3-calendar-heatmap": "^2.0.5"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0", "@intlify/unplugin-vue-i18n": "^4.0.0",
"@rushstack/eslint-patch": "^1.7.2", "@tsconfig/node18": "^18.2.4",
"@tsconfig/node18": "^18.2.2",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/node": "^20.11.19", "@types/node": "^20.12.10",
"@types/pulltorefreshjs": "^0.1.7", "@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.7", "@types/sortablejs": "^1.15.8",
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.4",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0", "eslint": "^9.2.0",
"eslint-plugin-vue": "^9.21.1", "eslint-plugin-vue": "^9.25.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pulltorefreshjs": "^0.1.22", "pulltorefreshjs": "^0.1.22",
"sass": "^1.71.0", "sass": "^1.76.0",
"terser": "^5.27.1", "terser": "^5.31.0",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.1.3", "vite": "^5.2.11",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.4.0", "vite-plugin-css-injected-by-js": "^3.5.1",
"vue-tsc": "^1.8.27" "vue-tsc": "^2.0.16"
} }
} }

View File

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

View File

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

View File

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

View File

@ -76,12 +76,12 @@ export default defineComponent({
}, },
productionYear() { productionYear() {
return() => { return() => {
return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014; return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014;
} }
}, },
productionWeek() { productionWeek() {
return() => { return() => {
return ((parseInt(this.devInfoList.serial.toString(), 16) >> (5 * 4)) & 0xFF).toString(16); return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xFF).toString(16);
} }
} }
} }

View File

@ -28,17 +28,20 @@
</tr> </tr>
<tr> <tr>
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th> <th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
<td v-if="modelAllowVersionInfo"> <td>
<a :href="systemStatus.update_url" target="_blank" v-tooltip <div class="form-check form-check-inline form-switch">
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
<label class="form-check-label">
<a v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined" :href="systemStatus.update_url" target="_blank" v-tooltip
:title="$t('firmwareinfo.FirmwareUpdateHint')"> :title="$t('firmwareinfo.FirmwareUpdateHint')">
<span class="badge" :class="systemStatus.update_status"> <span class="badge" :class="systemStatus.update_status">
{{ systemStatus.update_text }} {{ systemStatus.update_text }}
</span> </span>
</a> </a>
</td> <span v-else-if="modelAllowVersionInfo" class="badge" :class="systemStatus.update_status">
<td v-else> {{ systemStatus.update_text }}
<div class="form-check form-switch"> </span>
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" /> </label>
</div> </div>
</td> </td>
</tr> </tr>
@ -80,10 +83,10 @@ export default defineComponent({
}, },
computed: { computed: {
modelAllowVersionInfo: { modelAllowVersionInfo: {
get(): any { get(): boolean {
return !!this.allowVersionInfo; return !!this.allowVersionInfo;
}, },
set(value: any) { set(value: boolean) {
this.$emit('update:allowVersionInfo', value); this.$emit('update:allowVersionInfo', value);
}, },
}, },

View File

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

View File

@ -0,0 +1,116 @@
<template>
<input v-model="inputSerial" type="text" :id="id" :required="required" class="form-control" :class="inputClass" />
<BootstrapAlert show :variant="formatShow" v-if="formatHint">{{ formatHint }}</BootstrapAlert>
</template>
<script lang="ts">
import BootstrapAlert from './BootstrapAlert.vue';
import { defineComponent } from 'vue';
const chars32 = '0123456789ABCDEFGHJKLMNPRSTUVWXY';
export default defineComponent({
components: {
BootstrapAlert,
},
props: {
'modelValue': { type: [String, Number], required: true },
'id': String,
'inputClass': String,
'required': Boolean,
},
data() {
return {
inputSerial: "",
formatHint: "",
formatShow: "info",
};
},
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);
},
},
},
watch: {
modelValue: function (val) {
this.inputSerial = val;
},
inputSerial: function (val) {
const serial = val.toString().toUpperCase(); // Convert to lowercase for case-insensitivity
if (serial == "") {
this.formatHint = "";
this.model = "";
return;
}
this.formatShow = "info";
// Contains only numbers
if (/^1{1}[\dA-F]{11}$/.test(serial)) {
this.model = serial;
this.formatHint = this.$t('inputserial.format_hoymiles');
}
// Contains numbers and hex characters but at least one number
else if (/^(?=.*\d)[\dA-F]{12}$/.test(serial)) {
this.model = serial;
this.formatHint = this.$t('inputserial.format_converted');
}
// Has format: xxxxxxxxx-xxx
else if (/^((A01)|(A11)|(A21))[\dA-HJ-NR-YP]{6}-[\dA-HJ-NP-Z]{3}$/.test(serial)) {
if (this.checkHerfChecksum(serial)) {
this.model = this.convertHerfToHoy(serial);
this.$nextTick(() => {
this.formatHint = this.$t('inputserial.format_herf_valid', { serial: this.model });
});
} else {
this.formatHint = this.$t('inputserial.format_herf_invalid');
this.formatShow = "danger";
}
// Any other format
} else {
this.formatHint = this.$t('inputserial.format_unknown');
this.formatShow = "danger";
}
}
},
methods: {
checkHerfChecksum(sn: string) {
const chars64 = 'HMFLGW5XC301234567899Z67YRT2S8ABCDEFGHJKDVEJ4KQPUALMNPRSTUVWXYNB';
const checksum = sn.substring(sn.indexOf("-") + 1);
const serial = sn.substring(0, sn.indexOf("-"));
const first_char = '1';
const i = chars32.indexOf(first_char)
const sum1: number = Array.from(serial).reduce((sum, c) => sum + c.charCodeAt(0), 0) & 31;
const sum2: number = Array.from(serial).reduce((sum, c) => sum + chars32.indexOf(c), 0) & 31;
const ext = first_char + chars64[sum1 + i] + chars64[sum2 + i];
return checksum == ext;
},
convertHerfToHoy(sn: string) {
let sn_int: bigint = 0n;
for (let i = 0; i < 9; i++) {
const pos: bigint = BigInt(chars32.indexOf(sn[i].toUpperCase()));
const shift: bigint = BigInt(42 - 5 * i - (i <= 2 ? 0 : 2));
sn_int |= (pos << shift);
}
return sn_int.toString(16);
}
},
});
</script>

View File

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

View File

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

View File

@ -32,6 +32,9 @@
"Release": "Loslassen zum Aktualisieren", "Release": "Loslassen zum Aktualisieren",
"Close": "Schließen" "Close": "Schließen"
}, },
"Error": {
"Oops": "Oops!"
},
"localeswitcher": { "localeswitcher": {
"Dark": "Dunkel", "Dark": "Dunkel",
"Light": "Hell", "Light": "Hell",
@ -618,5 +621,12 @@
"Name": "Name", "Name": "Name",
"ValueSelected": "Ausgewählt", "ValueSelected": "Ausgewählt",
"ValueActive": "Aktiv" "ValueActive": "Aktiv"
},
"inputserial": {
"format_hoymiles": "Hoymiles Seriennummerformat",
"format_converted": "Bereits konvertierte Seriennummer",
"format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}",
"format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme",
"format_unknown": "Unbekanntes Format"
} }
} }

View File

@ -32,6 +32,9 @@
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Close" "Close": "Close"
}, },
"Error": {
"Oops": "Oops!"
},
"localeswitcher": { "localeswitcher": {
"Dark": "Dark", "Dark": "Dark",
"Light": "Light", "Light": "Light",
@ -619,5 +622,12 @@
"Number": "Number", "Number": "Number",
"ValueSelected": "Selected", "ValueSelected": "Selected",
"ValueActive": "Active" "ValueActive": "Active"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
} }
} }

View File

@ -32,6 +32,9 @@
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Fermer" "Close": "Fermer"
}, },
"Error": {
"Oops": "Oops!"
},
"localeswitcher": { "localeswitcher": {
"Dark": "Sombre", "Dark": "Sombre",
"Light": "Clair", "Light": "Clair",
@ -618,5 +621,12 @@
"Name": "Nom", "Name": "Nom",
"ValueSelected": "Sélectionné", "ValueSelected": "Sélectionné",
"ValueActive": "Activé" "ValueActive": "Activé"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"format_herf_valid": "E-Star HERF format (will be saved converted): {serial}",
"format_herf_invalid": "E-Star HERF format: Invalid checksum",
"format_unknown": "Unknown format"
} }
} }

View File

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

View File

@ -1,5 +1,5 @@
export interface DevInfoStatus { export interface DevInfoStatus {
serial: number; serial: string;
valid_data: boolean; valid_data: boolean;
fw_bootloader_version: number; fw_bootloader_version: number;
fw_build_version: number; fw_build_version: number;

View File

@ -6,7 +6,7 @@ export interface InverterChannel {
export interface Inverter { export interface Inverter {
id: string; id: string;
serial: number; serial: string;
name: string; name: string;
type: string; type: string;
order: number; order: number;

View File

@ -1,5 +1,5 @@
export interface LimitConfig { export interface LimitConfig {
serial: number; serial: string;
limit_value: number; limit_value: number;
limit_type: number; limit_type: number;
} }

View File

@ -22,7 +22,7 @@ export interface InverterStatistics {
} }
export interface Inverter { export interface Inverter {
serial: number; serial: string;
name: string; name: string;
order: number; order: number;
data_age: number; data_age: number;

View File

@ -41,7 +41,7 @@ export function isLoggedIn(): boolean {
return (localStorage.getItem('user') != null); return (localStorage.getItem('user') != null);
} }
export function login(username: String, password: String) { export function login(username: string, password: string) {
const requestOptions = { const requestOptions = {
method: 'GET', method: 'GET',
headers: { 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 => { return response.text().then(text => {
const data = text && JSON.parse(text); const data = text && JSON.parse(text);
if (!response.ok) { if (!response.ok) {
@ -74,9 +74,13 @@ export function handleResponse(response: Response, emitter: Emitter<Record<Event
logout(); logout();
emitter.emit("logged-out"); emitter.emit("logged-out");
router.push({ path: "/login", query: { returnUrl: router.currentRoute.value.fullPath } }); 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 }; 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); return Promise.reject(error);
} }

View File

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

View File

@ -56,7 +56,7 @@ export default defineComponent({
watch: { watch: {
consoleBuffer() { consoleBuffer() {
if (this.isAutoScroll) { if (this.isAutoScroll) {
let textarea = this.$el.querySelector("#console"); const textarea = this.$el.querySelector("#console");
setTimeout(() => { setTimeout(() => {
textarea.scrollTop = textarea.scrollHeight; textarea.scrollTop = textarea.scrollHeight;
}, 0); }, 0);
@ -121,7 +121,7 @@ export default defineComponent({
this.heartInterval && clearTimeout(this.heartInterval); this.heartInterval && clearTimeout(this.heartInterval);
}, },
getOutDate(): String { getOutDate(): string {
const u = new Date(); const u = new Date();
return ('0' + u.getHours()).slice(-2) + ':' + return ('0' + u.getHours()).slice(-2) + ':' +
('0' + u.getMinutes()).slice(-2) + ':' + ('0' + u.getMinutes()).slice(-2) + ':' +
@ -132,7 +132,7 @@ export default defineComponent({
this.consoleBuffer = ""; this.consoleBuffer = "";
}, },
copyConsole() { copyConsole() {
var input = document.createElement('textarea'); const input = document.createElement('textarea');
input.innerHTML = this.consoleBuffer; input.innerHTML = this.consoleBuffer;
document.body.appendChild(input); document.body.appendChild(input);
input.select(); input.select();

View File

@ -219,7 +219,7 @@ export default defineComponent({
getPinMappingList() { getPinMappingList() {
this.pinMappingLoading = true; this.pinMappingLoading = true;
fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() }) 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( .then(
(data) => { (data) => {
this.pinMappingList = data; this.pinMappingList = data;
@ -246,6 +246,9 @@ export default defineComponent({
.then( .then(
(data) => { (data) => {
this.deviceConfigList = data; this.deviceConfigList = data;
if (this.deviceConfigList.curPin.name === "") {
this.deviceConfigList.curPin.name = "Default";
}
this.dataLoading = false; 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"; const remoteHostUrl = "/api/system/status";
// Use a simple fetch request to check if the remote host is reachable // Use a simple fetch request to check if the remote host is reachable
fetch(remoteHostUrl, { method: 'HEAD' }) fetch(remoteHostUrl, { method: 'GET' })
.then(response => { .then(response => {
// Check if the response status is OK (200-299 range) // Check if the response status is OK (200-299 range)
if (response.ok) { if (response.ok) {

View File

@ -11,14 +11,20 @@
<div class="row gy-3"> <div class="row gy-3">
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]"> <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"> <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" :id="'v-pills-' + inverter.serial + '-tab'" data-bs-toggle="pill"
:data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab" :data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab"
aria-controls="'v-pills-' + inverter.serial" aria-selected="true"> aria-controls="'v-pills-' + inverter.serial" aria-selected="true">
<div class="row">
<div class="col-auto col-sm-2">
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" /> <BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" /> <BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
<BIconCheckCircleFill 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 }} {{ inverter.name }}
</div>
</div>
</button> </button>
</div> </div>
</div> </div>
@ -359,7 +365,7 @@ export default defineComponent({
showAlertLimit: false, showAlertLimit: false,
powerSettingView: {} as bootstrap.Modal, powerSettingView: {} as bootstrap.Modal,
powerSettingSerial: 0, powerSettingSerial: "",
powerSettingLoading: true, powerSettingLoading: true,
alertMessagePower: "", alertMessagePower: "",
alertTypePower: "info", alertTypePower: "info",
@ -483,17 +489,15 @@ export default defineComponent({
} }
}; };
var self = this; this.socket.onopen = (event) => {
this.socket.onopen = function (event) {
console.log(event); console.log(event);
console.log("Successfully connected to the echo websocket server..."); 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...") 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 // Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
@ -528,7 +532,7 @@ export default defineComponent({
this.heartInterval && clearTimeout(this.heartInterval); this.heartInterval && clearTimeout(this.heartInterval);
this.isFirstFetchAfterConnect = true; this.isFirstFetchAfterConnect = true;
}, },
onShowEventlog(serial: number) { onShowEventlog(serial: string) {
this.eventLogLoading = true; this.eventLogLoading = true;
fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() }) fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
@ -539,7 +543,7 @@ export default defineComponent({
this.eventLogView.show(); this.eventLogView.show();
}, },
onShowDevInfo(serial: number) { onShowDevInfo(serial: string) {
this.devInfoLoading = true; this.devInfoLoading = true;
fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() }) fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
@ -551,7 +555,7 @@ export default defineComponent({
this.devInfoView.show(); this.devInfoView.show();
}, },
onShowGridProfile(serial: number) { onShowGridProfile(serial: string) {
this.gridProfileLoading = true; this.gridProfileLoading = true;
fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() }) fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
@ -568,9 +572,9 @@ export default defineComponent({
this.gridProfileView.show(); this.gridProfileView.show();
}, },
onShowLimitSettings(serial: number) { onShowLimitSettings(serial: string) {
this.showAlertLimit = false; this.showAlertLimit = false;
this.targetLimitList.serial = 0; this.targetLimitList.serial = "";
this.targetLimitList.limit_value = 0; this.targetLimitList.limit_value = 0;
this.targetLimitType = 1; this.targetLimitType = 1;
this.targetLimitTypeText = this.$t('home.Relative'); this.targetLimitTypeText = this.$t('home.Relative');
@ -624,9 +628,9 @@ export default defineComponent({
this.targetLimitType = type; this.targetLimitType = type;
}, },
onShowPowerSettings(serial: number) { onShowPowerSettings(serial: string) {
this.showAlertPower = false; this.showAlertPower = false;
this.powerSettingSerial = 0; this.powerSettingSerial = "";
this.powerSettingLoading = true; this.powerSettingLoading = true;
fetch("/api/power/status", { headers: authHeader() }) fetch("/api/power/status", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))

View File

@ -8,8 +8,7 @@
<form class="form-inline" v-on:submit.prevent="onSubmit"> <form class="form-inline" v-on:submit.prevent="onSubmit">
<div class="form-group"> <div class="form-group">
<label>{{ $t('inverteradmin.Serial') }}</label> <label>{{ $t('inverteradmin.Serial') }}</label>
<input v-model="newInverterData.serial" type="number" class="form-control ml-sm-2 mr-sm-4 my-2" <InputSerial v-model="newInverterData.serial" inputClass="ml-sm-2 mr-sm-4 my-2" required />
required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>{{ $t('inverteradmin.Name') }}</label> <label>{{ $t('inverteradmin.Name') }}</label>
@ -91,7 +90,7 @@
<label for="inverter-serial" class="col-form-label"> <label for="inverter-serial" class="col-form-label">
{{ $t('inverteradmin.InverterSerial') }} {{ $t('inverteradmin.InverterSerial') }}
</label> </label>
<input v-model="selectedInverterData.serial" type="number" id="inverter-serial" class="form-control" /> <InputSerial v-model="selectedInverterData.serial" id="inverter-serial" />
<label for="inverter-name" class="col-form-label">{{ $t('inverteradmin.InverterName') }} <label for="inverter-name" class="col-form-label">{{ $t('inverteradmin.InverterName') }}
<BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" /> <BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" />
</label> </label>
@ -207,6 +206,7 @@ import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import InputSerial from '@/components/InputSerial.vue';
import ModalDialog from '@/components/ModalDialog.vue'; import ModalDialog from '@/components/ModalDialog.vue';
import type { Inverter } from '@/types/InverterConfig'; import type { Inverter } from '@/types/InverterConfig';
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
@ -235,6 +235,7 @@ export default defineComponent({
BootstrapAlert, BootstrapAlert,
CardElement, CardElement,
InputElement, InputElement,
InputSerial,
ModalDialog, ModalDialog,
BIconInfoCircle, BIconInfoCircle,
BIconPencil, BIconPencil,

View File

@ -58,12 +58,16 @@ export default defineComponent({
}) })
}, },
getUpdateInfo() { getUpdateInfo() {
if (this.systemDataList.git_hash === undefined) {
return;
}
// If the left char is a "g" the value is the git hash (remove the "g") // If the left char is a "g" the value is the git hash (remove the "g")
this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g'; this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g';
this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash; this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash;
// Handle format "v0.1-5-gabcdefh" // Handle format "v0.1-5-gabcdefh"
if (this.systemDataList.git_hash.lastIndexOf("-") >= 0) { if (this.systemDataList.git_hash?.lastIndexOf("-") >= 0) {
this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2) this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2)
this.systemDataList.git_is_hash = true; this.systemDataList.git_is_hash = true;
} }
@ -95,9 +99,9 @@ export default defineComponent({
} }
}, },
watch: { watch: {
allowVersionInfo(allow: Boolean) { allowVersionInfo(allow: boolean) {
localStorage.setItem("allowVersionInfo", allow ? "1" : "0");
if (allow) { if (allow) {
localStorage.setItem("allowVersionInfo", this.allowVersionInfo ? "1" : "0");
this.getUpdateInfo(); this.getUpdateInfo();
} }
} }

View File

@ -12,6 +12,7 @@ import path from 'path'
// example 'vite.user.ts': export const proxy_target = '192.168.16.107' // example 'vite.user.ts': export const proxy_target = '192.168.16.107'
let proxy_target; let proxy_target;
try { try {
// eslint-disable-next-line
proxy_target = require('./vite.user.ts').proxy_target; proxy_target = require('./vite.user.ts').proxy_target;
} catch (error) { } catch (error) {
proxy_target = '192.168.2.93'; proxy_target = '192.168.2.93';
@ -29,6 +30,7 @@ export default defineConfig({
fullInstall: false, fullInstall: false,
forceStringify: true, forceStringify: true,
strictMessage: false, strictMessage: false,
jitCompilation: false,
}), }),
], ],
resolve: { resolve: {

Some files were not shown because too many files have changed in this diff Show More