Merge https://github.com/RaBa64/OpenDTU-Database into Database
This commit is contained in:
commit
d6c183c6d6
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -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
54
.github/workflows/repo-maintenance.yml
vendored
Normal 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.
|
||||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -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": [
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
# OpenDTU-Database-Database
|
# OpenDTU-Database-Database
|
||||||
|
# OpenDTU-Database
|
||||||
|
|
||||||
|
One year OpenDTU-Database
|
||||||
|

|
||||||
|
|
||||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
||||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
|
[](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 |
|
||||||
|
|||||||
BIN
docs/screenshots/Screenshot_2024-05-23_131208.png
Normal file
BIN
docs/screenshots/Screenshot_2024-05-23_131208.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
8
include/__compiled_constants.h
Normal file
8
include/__compiled_constants.h
Normal 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__;
|
||||||
@ -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
|
||||||
|
|||||||
@ -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) {
|
||||||
@ -271,4 +277,4 @@ void HoymilesClass::setMessageOutput(Print* output)
|
|||||||
Print* HoymilesClass::getMessageOutput()
|
Print* HoymilesClass::getMessageOutput()
|
||||||
{
|
{
|
||||||
return _messageOutput;
|
return _messageOutput;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
@ -43,4 +43,4 @@ protected:
|
|||||||
bool _busyFlag = false;
|
bool _busyFlag = false;
|
||||||
|
|
||||||
TimeoutHelper _rxTimeout;
|
TimeoutHelper _rxTimeout;
|
||||||
};
|
};
|
||||||
|
|||||||
62
lib/Hoymiles/src/inverters/HERF_2CH.cpp
Normal file
62
lib/Hoymiles/src/inverters/HERF_2CH.cpp
Normal 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]);
|
||||||
|
}
|
||||||
13
lib/Hoymiles/src/inverters/HERF_2CH.h
Normal file
13
lib/Hoymiles/src/inverters/HERF_2CH.h
Normal 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;
|
||||||
|
};
|
||||||
20
lib/Hoymiles/src/inverters/HERF_4CH.cpp
Normal file
20
lib/Hoymiles/src/inverters/HERF_4CH.cpp
Normal 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";
|
||||||
|
}
|
||||||
11
lib/Hoymiles/src/inverters/HERF_4CH.h
Normal file
11
lib/Hoymiles/src/inverters/HERF_4CH.h
Normal 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;
|
||||||
|
};
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 |
|
||||||
|
|||||||
@ -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", "", "" },
|
||||||
@ -294,4 +295,4 @@ int AlarmLogParser::getTimezoneOffset()
|
|||||||
gmt = mktime(ptm);
|
gmt = mktime(ptm);
|
||||||
|
|
||||||
return static_cast<int>(difftime(rawtime, gmt));
|
return static_cast<int>(difftime(rawtime, gmt));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -62,4 +62,4 @@ private:
|
|||||||
AlarmMessageType_t _messageType = AlarmMessageType_t::ALL;
|
AlarmMessageType_t _messageType = AlarmMessageType_t::ALL;
|
||||||
|
|
||||||
static const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> _alarmMessages;
|
static const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> _alarmMessages;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
@ -52,4 +54,4 @@ private:
|
|||||||
|
|
||||||
static const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> _profileTypes;
|
static const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> _profileTypes;
|
||||||
static const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> _profileValues;
|
static const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> _profileValues;
|
||||||
};
|
};
|
||||||
|
|||||||
0
lib/ThreadSafeQueue/README.md
Normal file
0
lib/ThreadSafeQueue/README.md
Normal file
13
lib/ThreadSafeQueue/library.json
Normal file
13
lib/ThreadSafeQueue/library.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
0
lib/TimeoutHelper/README.md
Normal file
0
lib/TimeoutHelper/README.md
Normal file
13
lib/TimeoutHelper/library.json
Normal file
13
lib/TimeoutHelper/library.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
patches/async_tcp/event_queue_size.patch
Normal file
26
patches/async_tcp/event_queue_size.patch
Normal 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
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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(
|
|
||||||
BUILD_FLAGS=[get_firmware_specifier_build_flag()]
|
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()]
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
@ -216,4 +214,4 @@ bool PinMappingClass::isValidCmt2300Config() const
|
|||||||
bool PinMappingClass::isValidEthConfig() const
|
bool PinMappingClass::isValidEthConfig() const
|
||||||
{
|
{
|
||||||
return _pinMapping.eth_enabled;
|
return _pinMapping.eth_enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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
36
webapp/eslint.config.js
Normal 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'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
@ -116,4 +116,4 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -76,14 +76,14 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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">
|
||||||
:title="$t('firmwareinfo.FirmwareUpdateHint')">
|
|
||||||
<span class="badge" :class="systemStatus.update_status">
|
|
||||||
{{ systemStatus.update_text }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td v-else>
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
|
<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')">
|
||||||
|
<span class="badge" :class="systemStatus.update_status">
|
||||||
|
{{ systemStatus.update_text }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<span v-else-if="modelAllowVersionInfo" class="badge" :class="systemStatus.update_status">
|
||||||
|
{{ systemStatus.update_text }}
|
||||||
|
</span>
|
||||||
|
</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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
@ -112,4 +114,4 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
116
webapp/src/components/InputSerial.vue
Normal file
116
webapp/src/components/InputSerial.vue
Normal 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>
|
||||||
@ -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,18 +170,18 @@ 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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
@ -115,4 +121,4 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -8,4 +8,4 @@ export interface DevInfoStatus {
|
|||||||
hw_version: number;
|
hw_version: number;
|
||||||
hw_model_name: string;
|
hw_model_name: string;
|
||||||
max_power: number;
|
max_power: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -53,4 +53,4 @@ export interface LiveData {
|
|||||||
inverters: Inverter[];
|
inverters: Inverter[];
|
||||||
total: Total;
|
total: Total;
|
||||||
hints: Hints;
|
hints: Hints;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,4 +103,4 @@ function handleAuthResponse(response: Response) {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -1,154 +1,154 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
|
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
|
||||||
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
|
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
|
||||||
<div class="row g-3 align-items-center">
|
<div class="row g-3 align-items-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
|
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
|
||||||
v-model="isAutoScroll">
|
v-model="isAutoScroll">
|
||||||
<label class="form-check-label" for="autoScroll">
|
<label class="form-check-label" for="autoScroll">
|
||||||
{{ $t('console.EnableAutoScroll') }}
|
{{ $t('console.EnableAutoScroll') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-end">
|
<div class="col text-end">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button type="button" class="btn btn-primary" :onClick="clearConsole">
|
<button type="button" class="btn btn-primary" :onClick="clearConsole">
|
||||||
{{ $t('console.ClearConsole') }}</button>
|
{{ $t('console.ClearConsole') }}</button>
|
||||||
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
|
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
|
||||||
{{ $t('console.CopyToClipboard') }}</button>
|
{{ $t('console.CopyToClipboard') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
|
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
|
||||||
</CardElement>
|
</CardElement>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BasePage from '@/components/BasePage.vue';
|
import BasePage from '@/components/BasePage.vue';
|
||||||
import CardElement from '@/components/CardElement.vue';
|
import CardElement from '@/components/CardElement.vue';
|
||||||
import { authUrl } from '@/utils/authentication';
|
import { authUrl } from '@/utils/authentication';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
BasePage,
|
BasePage,
|
||||||
CardElement,
|
CardElement,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
socket: {} as WebSocket,
|
socket: {} as WebSocket,
|
||||||
heartInterval: 0,
|
heartInterval: 0,
|
||||||
dataLoading: true,
|
dataLoading: true,
|
||||||
consoleBuffer: "",
|
consoleBuffer: "",
|
||||||
isAutoScroll: true,
|
isAutoScroll: true,
|
||||||
endWithNewline: false,
|
endWithNewline: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.initSocket();
|
this.initSocket();
|
||||||
this.dataLoading = false;
|
this.dataLoading = false;
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
this.closeSocket();
|
this.closeSocket();
|
||||||
},
|
},
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initSocket() {
|
initSocket() {
|
||||||
console.log("Starting connection to WebSocket Server");
|
console.log("Starting connection to WebSocket Server");
|
||||||
|
|
||||||
const { protocol, host } = location;
|
const { protocol, host } = location;
|
||||||
const authString = authUrl();
|
const authString = authUrl();
|
||||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||||
}://${authString}${host}/console`;
|
}://${authString}${host}/console`;
|
||||||
|
|
||||||
this.closeSocket();
|
this.closeSocket();
|
||||||
this.socket = new WebSocket(webSocketUrl);
|
this.socket = new WebSocket(webSocketUrl);
|
||||||
|
|
||||||
this.socket.onmessage = (event) => {
|
this.socket.onmessage = (event) => {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
|
|
||||||
let outstr = new String(event.data);
|
let outstr = new String(event.data);
|
||||||
let removedNewline = false;
|
let removedNewline = false;
|
||||||
if (outstr.endsWith('\n')) {
|
if (outstr.endsWith('\n')) {
|
||||||
outstr = outstr.substring(0, outstr.length - 1);
|
outstr = outstr.substring(0, outstr.length - 1);
|
||||||
removedNewline = true;
|
removedNewline = true;
|
||||||
}
|
}
|
||||||
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
|
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
|
||||||
this.endWithNewline = removedNewline;
|
this.endWithNewline = removedNewline;
|
||||||
this.heartCheck(); // Reset heartbeat detection
|
this.heartCheck(); // Reset heartbeat detection
|
||||||
};
|
};
|
||||||
|
|
||||||
this.socket.onopen = function (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...");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
window.onbeforeunload = () => {
|
window.onbeforeunload = () => {
|
||||||
this.closeSocket();
|
this.closeSocket();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||||
heartCheck() {
|
heartCheck() {
|
||||||
this.heartInterval && clearTimeout(this.heartInterval);
|
this.heartInterval && clearTimeout(this.heartInterval);
|
||||||
this.heartInterval = setInterval(() => {
|
this.heartInterval = setInterval(() => {
|
||||||
if (this.socket.readyState === 1) {
|
if (this.socket.readyState === 1) {
|
||||||
// Connection status
|
// Connection status
|
||||||
this.socket.send("ping");
|
this.socket.send("ping");
|
||||||
} else {
|
} else {
|
||||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||||
}
|
}
|
||||||
}, 5 * 1000);
|
}, 5 * 1000);
|
||||||
},
|
},
|
||||||
/** To break off websocket Connect */
|
/** To break off websocket Connect */
|
||||||
closeSocket() {
|
closeSocket() {
|
||||||
try {
|
try {
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
} catch {
|
} catch {
|
||||||
// continue regardless of error
|
// continue regardless of error
|
||||||
}
|
}
|
||||||
|
|
||||||
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) + ':' +
|
||||||
('0' + u.getSeconds()).slice(-2) + '.' +
|
('0' + u.getSeconds()).slice(-2) + '.' +
|
||||||
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
|
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
|
||||||
},
|
},
|
||||||
clearConsole() {
|
clearConsole() {
|
||||||
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();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
document.body.removeChild(input);
|
document.body.removeChild(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#console {
|
#console {
|
||||||
background-color: #0C0C0C;
|
background-color: #0C0C0C;
|
||||||
color: #CCCCCC;
|
color: #CCCCCC;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-family: courier new;
|
font-family: courier new;
|
||||||
font-size: .875em;
|
font-size: .875em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
18
webapp/src/views/ErrorView.vue
Normal file
18
webapp/src/views/ErrorView.vue
Normal 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>
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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">
|
||||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
<div class="row">
|
||||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
<div class="col-auto col-sm-2">
|
||||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||||
{{ inverter.name }}
|
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||||
|
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
{{ inverter.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</button>
|
</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))
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user