Merge branch 'tbnobody:master' into master
This commit is contained in:
commit
ac88d4f822
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -119,7 +119,7 @@ jobs:
|
||||
|
||||
- name: Build Changelog
|
||||
id: github_release
|
||||
uses: mikepenz/release-changelog-builder-action@v3
|
||||
uses: mikepenz/release-changelog-builder-action@v4
|
||||
with:
|
||||
failOnError: true
|
||||
commitMode: true
|
||||
@ -138,7 +138,7 @@ jobs:
|
||||
for i in */; do cp ${i}opendtu-*.bin ./; done
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: ${{steps.github_release.outputs.changelog}}
|
||||
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",
|
||||
"EditorConfig.EditorConfig",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
|
||||
@ -78,3 +78,6 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre
|
||||
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
|
||||
| TSUN TSOL-M800 | NRF24L01+ | 2 | 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 |
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
#define CONFIG_FILENAME "/config.json"
|
||||
#define CONFIG_VERSION 0x00011b00 // 0.1.27 // make sure to clean all after change
|
||||
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change
|
||||
|
||||
#define WIFI_MAX_SSID_STRLEN 32
|
||||
#define WIFI_MAX_PASSWORD_STRLEN 64
|
||||
@ -30,8 +30,6 @@
|
||||
|
||||
#define DEV_MAX_MAPPING_NAME_STRLEN 63
|
||||
|
||||
#define JSON_BUFFER_SIZE 12288
|
||||
|
||||
struct CHANNEL_CONFIG_T {
|
||||
uint16_t MaxChannelPower;
|
||||
char Name[CHAN_MAX_NAME_STRLEN];
|
||||
@ -168,6 +166,7 @@ public:
|
||||
|
||||
INVERTER_CONFIG_T* getFreeInverterSlot();
|
||||
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
|
||||
void deleteInverterById(const uint8_t id);
|
||||
};
|
||||
|
||||
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 publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off);
|
||||
|
||||
static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
|
||||
static void createDtuInfo(DynamicJsonDocument& doc);
|
||||
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
|
||||
static void createDtuInfo(JsonDocument& doc);
|
||||
|
||||
static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = "");
|
||||
static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = "");
|
||||
|
||||
static String getDtuUniqueId();
|
||||
static String getDtuUrl();
|
||||
|
||||
@ -10,6 +10,6 @@ public:
|
||||
static uint64_t generateDtuSerial();
|
||||
static int getTimezoneOffset();
|
||||
static void restartDtu();
|
||||
static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line);
|
||||
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
|
||||
static void removeAllFiles();
|
||||
};
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
#include "WebApi_webapp.h"
|
||||
#include "WebApi_ws_console.h"
|
||||
#include "WebApi_ws_live.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
@ -37,6 +38,10 @@ public:
|
||||
|
||||
static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!");
|
||||
|
||||
static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document);
|
||||
static uint64_t parseSerialFromRequest(AsyncWebServerRequest* request, String param_name = "inv");
|
||||
static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line);
|
||||
|
||||
private:
|
||||
AsyncWebServer _server;
|
||||
|
||||
|
||||
@ -5,10 +5,11 @@ enum WebApiError {
|
||||
GenericBase = 1000,
|
||||
GenericSuccess,
|
||||
GenericNoValueFound,
|
||||
GenericDataTooLarge,
|
||||
GenericDataTooLarge, // not used anymore
|
||||
GenericParseError,
|
||||
GenericValueMissing,
|
||||
GenericWriteFailed,
|
||||
GenericInternalServerError,
|
||||
|
||||
DtuBase = 2000,
|
||||
DtuSerialZero,
|
||||
|
||||
@ -4,8 +4,6 @@
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
|
||||
#define MQTT_JSON_DOC_SIZE 10240
|
||||
|
||||
class WebApiMqttClass {
|
||||
public:
|
||||
void init(AsyncWebServer& server, Scheduler& scheduler);
|
||||
|
||||
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 NTP_SERVER "pool.ntp.org"
|
||||
#define NTP_SERVER_OLD "pool.ntp.org"
|
||||
#define NTP_SERVER "opendtu.pool.ntp.org"
|
||||
#define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||
#define NTP_TIMEZONEDESCR "Europe/Berlin"
|
||||
#define NTP_LONGITUDE 10.4515f
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
// 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 "Utils.h"
|
||||
#include "inverters/HERF_2CH.h"
|
||||
#include "inverters/HERF_4CH.h"
|
||||
#include "inverters/HMS_1CH.h"
|
||||
#include "inverters/HMS_1CHv2.h"
|
||||
#include "inverters/HMS_2CH.h"
|
||||
@ -112,7 +114,7 @@ void HoymilesClass::loop()
|
||||
}
|
||||
|
||||
// Fetch grid profile
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) {
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->GridProfile()->getLastUpdate() == 0 || !iv->GridProfile()->containsValidData())) {
|
||||
iv->sendGridOnProFileParaRequest();
|
||||
}
|
||||
|
||||
@ -168,6 +170,10 @@ std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, c
|
||||
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
|
||||
} else if (HM_1CH::isValidSerial(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) {
|
||||
@ -271,4 +277,4 @@ void HoymilesClass::setMessageOutput(Print* output)
|
||||
Print* HoymilesClass::getMessageOutput()
|
||||
{
|
||||
return _messageOutput;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "TimeoutHelper.h"
|
||||
#include "commands/CommandAbstract.h"
|
||||
#include "types.h"
|
||||
#include <memory>
|
||||
#include <ThreadSafeQueue.h>
|
||||
#include <TimeoutHelper.h>
|
||||
#include <memory>
|
||||
|
||||
class HoymilesRadio {
|
||||
public:
|
||||
@ -43,4 +43,4 @@ protected:
|
||||
bool _busyFlag = false;
|
||||
|
||||
TimeoutHelper _rxTimeout;
|
||||
};
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
// serial >= 0x112400000000 && serial <= 0x112499999999
|
||||
// serial >= 0x112400000000 && serial <= 0x1124ffffffff
|
||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||
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)
|
||||
{
|
||||
// serial >= 0x112500000000 && serial <= 0x112599999999
|
||||
// serial >= 0x112500000000 && serial <= 0x1125ffffffff
|
||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||
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)
|
||||
{
|
||||
// serial >= 0x114400000000 && serial <= 0x114499999999
|
||||
// serial >= 0x114400000000 && serial <= 0x1144ffffffff
|
||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||
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)
|
||||
{
|
||||
// serial >= 0x116400000000 && serial <= 0x116499999999
|
||||
// serial >= 0x116400000000 && serial <= 0x1164ffffffff
|
||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||
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)
|
||||
{
|
||||
// serial >= 0x136100000000 && serial <= 0x136199999999
|
||||
// serial >= 0x136100000000 && serial <= 0x1361ffffffff
|
||||
uint16_t preSerial = (serial >> 32) & 0xffff;
|
||||
return preSerial == 0x1361;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -77,14 +77,14 @@ HMT_6CH::HMT_6CH(HoymilesRadio* radio, 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;
|
||||
return preSerial == 0x1382;
|
||||
}
|
||||
|
||||
String HMT_6CH::typeName() const
|
||||
{
|
||||
return F("HMT-1800/2250-6T");
|
||||
return "HMT-1800/2250-6T";
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// serial >= 0x112100000000 && serial <= 0x112199999999
|
||||
// serial >= 0x112100000000 && serial <= 0x1121ffffffff
|
||||
|
||||
uint8_t preId[2];
|
||||
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)
|
||||
{
|
||||
// serial >= 0x114100000000 && serial <= 0x114199999999
|
||||
// serial >= 0x114100000000 && serial <= 0x1141ffffffff
|
||||
|
||||
uint8_t preId[2];
|
||||
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)
|
||||
{
|
||||
// serial >= 0x116100000000 && serial <= 0x116199999999
|
||||
// serial >= 0x116100000000 && serial <= 0x1161ffffffff
|
||||
|
||||
uint8_t preId[2];
|
||||
preId[0] = (uint8_t)(serial >> 40);
|
||||
|
||||
@ -11,3 +11,5 @@
|
||||
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
|
||||
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
|
||||
| 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, 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, 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, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection d’îlots" },
|
||||
|
||||
{ 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::ALL, 181, "Abnormal insulation impedance", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" },
|
||||
@ -294,4 +295,4 @@ int AlarmLogParser::getTimezoneOffset()
|
||||
gmt = mktime(ptm);
|
||||
|
||||
return static_cast<int>(difftime(rawtime, gmt));
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
#define ALARM_LOG_ENTRY_SIZE 12
|
||||
#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 {
|
||||
uint16_t MessageId;
|
||||
@ -62,4 +62,4 @@ private:
|
||||
AlarmMessageType_t _messageType = AlarmMessageType_t::ALL;
|
||||
|
||||
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, 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()
|
||||
@ -200,7 +204,7 @@ bool DevInfoParser::containsValidData() const
|
||||
struct tm info;
|
||||
localtime_r(&t, &info);
|
||||
|
||||
return info.tm_year > (2016 - 1900);
|
||||
return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097;
|
||||
}
|
||||
|
||||
uint8_t DevInfoParser::getDevIdx() const
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2023 Thomas Basler and others
|
||||
* Copyright (C) 2023 - 2024 Thomas Basler and others
|
||||
*/
|
||||
#include "GridProfileParser.h"
|
||||
#include "../Hoymiles.h"
|
||||
@ -446,6 +446,11 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
|
||||
return l;
|
||||
}
|
||||
|
||||
bool GridProfileParser::containsValidData() const
|
||||
{
|
||||
return _gridProfileLength > 6;
|
||||
}
|
||||
|
||||
uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version)
|
||||
{
|
||||
uint8_t count = 0;
|
||||
|
||||
@ -43,6 +43,8 @@ public:
|
||||
|
||||
std::list<GridProfileSection_t> getProfile() const;
|
||||
|
||||
bool containsValidData() const;
|
||||
|
||||
private:
|
||||
static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version);
|
||||
static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version);
|
||||
@ -52,4 +54,4 @@ private:
|
||||
|
||||
static const std::array<const ProfileType_t, PROFILE_TYPE_COUNT> _profileTypes;
|
||||
static const std::array<const GridProfileValue_t, SECTION_VALUE_COUNT> _profileValues;
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
#
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
Import("env")
|
||||
@ -15,15 +16,64 @@ if missing_pkgs:
|
||||
|
||||
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:
|
||||
build_version = porcelain.describe('.') # '.' refers to the repository root dir
|
||||
except:
|
||||
build_version = "g0000000"
|
||||
build_flag = "-D AUTO_GIT_HASH=\\\"" + 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)
|
||||
|
||||
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
|
||||
|
||||
framework = arduino
|
||||
platform = espressif32@6.5.0
|
||||
platform = espressif32@6.6.0
|
||||
|
||||
build_flags =
|
||||
-DPIOENV=\"$PIOENV\"
|
||||
-D_TASK_STD_FUNCTION=1
|
||||
-D_TASK_THREAD_SAFE=1
|
||||
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
|
||||
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
|
||||
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
|
||||
; Have to remove -Werror because of
|
||||
; https://github.com/espressif/arduino-esp32/issues/9044 and
|
||||
@ -36,12 +38,12 @@ build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
lib_deps =
|
||||
mathieucarbou/ESP Async WebServer @ 2.7.0
|
||||
bblanchon/ArduinoJson @ ^6.21.5
|
||||
mathieucarbou/ESP Async WebServer @ 2.9.5
|
||||
bblanchon/ArduinoJson @ 7.0.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.6.0
|
||||
nrf24/RF24 @ ^1.4.8
|
||||
olikraus/U8g2 @ ^2.35.9
|
||||
buelowp/sunset @ ^1.1.7
|
||||
nrf24/RF24 @ 1.4.8
|
||||
olikraus/U8g2 @ 2.35.19
|
||||
buelowp/sunset @ 1.1.7
|
||||
https://github.com/arkhipenko/TaskScheduler#testing
|
||||
|
||||
extra_scripts =
|
||||
@ -59,7 +61,7 @@ board_build.embed_files =
|
||||
webapp_dist/js/app.js.gz
|
||||
webapp_dist/site.webmanifest
|
||||
|
||||
custom_patches =
|
||||
custom_patches = async_tcp
|
||||
|
||||
monitor_filters = esp32_exception_decoder, time, log2file, colorize
|
||||
monitor_speed = 115200
|
||||
@ -87,13 +89,13 @@ build_flags = ${env.build_flags}
|
||||
|
||||
[env:generic_esp32c3]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
custom_patches = ${env.custom_patches}
|
||||
build_flags = ${env.build_flags}
|
||||
|
||||
|
||||
[env:generic_esp32c3_usb]
|
||||
board = esp32-c3-devkitc-02
|
||||
custom_patches = ${env.custom_patches},esp32c3
|
||||
custom_patches = ${env.custom_patches}
|
||||
build_flags = ${env.build_flags}
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
|
||||
@ -25,17 +25,13 @@ bool ConfigurationClass::write()
|
||||
}
|
||||
config.Cfg.SaveCount++;
|
||||
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
JsonDocument doc;
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject cfg = doc.createNestedObject("cfg");
|
||||
JsonObject cfg = doc["cfg"].to<JsonObject>();
|
||||
cfg["version"] = config.Cfg.Version;
|
||||
cfg["save_count"] = config.Cfg.SaveCount;
|
||||
|
||||
JsonObject wifi = doc.createNestedObject("wifi");
|
||||
JsonObject wifi = doc["wifi"].to<JsonObject>();
|
||||
wifi["ssid"] = config.WiFi.Ssid;
|
||||
wifi["password"] = config.WiFi.Password;
|
||||
wifi["ip"] = IPAddress(config.WiFi.Ip).toString();
|
||||
@ -47,10 +43,10 @@ bool ConfigurationClass::write()
|
||||
wifi["hostname"] = config.WiFi.Hostname;
|
||||
wifi["aptimeout"] = config.WiFi.ApTimeout;
|
||||
|
||||
JsonObject mdns = doc.createNestedObject("mdns");
|
||||
JsonObject mdns = doc["mdns"].to<JsonObject>();
|
||||
mdns["enabled"] = config.Mdns.Enabled;
|
||||
|
||||
JsonObject ntp = doc.createNestedObject("ntp");
|
||||
JsonObject ntp = doc["ntp"].to<JsonObject>();
|
||||
ntp["server"] = config.Ntp.Server;
|
||||
ntp["timezone"] = config.Ntp.Timezone;
|
||||
ntp["timezone_descr"] = config.Ntp.TimezoneDescr;
|
||||
@ -58,7 +54,7 @@ bool ConfigurationClass::write()
|
||||
ntp["longitude"] = config.Ntp.Longitude;
|
||||
ntp["sunsettype"] = config.Ntp.SunsetType;
|
||||
|
||||
JsonObject mqtt = doc.createNestedObject("mqtt");
|
||||
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
|
||||
mqtt["enabled"] = config.Mqtt.Enabled;
|
||||
mqtt["hostname"] = config.Mqtt.Hostname;
|
||||
mqtt["port"] = config.Mqtt.Port;
|
||||
@ -69,27 +65,27 @@ bool ConfigurationClass::write()
|
||||
mqtt["publish_interval"] = config.Mqtt.PublishInterval;
|
||||
mqtt["clean_session"] = config.Mqtt.CleanSession;
|
||||
|
||||
JsonObject mqtt_lwt = mqtt.createNestedObject("lwt");
|
||||
JsonObject mqtt_lwt = mqtt["lwt"].to<JsonObject>();
|
||||
mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic;
|
||||
mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online;
|
||||
mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline;
|
||||
mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos;
|
||||
|
||||
JsonObject mqtt_tls = mqtt.createNestedObject("tls");
|
||||
JsonObject mqtt_tls = mqtt["tls"].to<JsonObject>();
|
||||
mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled;
|
||||
mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert;
|
||||
mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin;
|
||||
mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert;
|
||||
mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey;
|
||||
|
||||
JsonObject mqtt_hass = mqtt.createNestedObject("hass");
|
||||
JsonObject mqtt_hass = mqtt["hass"].to<JsonObject>();
|
||||
mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled;
|
||||
mqtt_hass["retain"] = config.Mqtt.Hass.Retain;
|
||||
mqtt_hass["topic"] = config.Mqtt.Hass.Topic;
|
||||
mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels;
|
||||
mqtt_hass["expire"] = config.Mqtt.Hass.Expire;
|
||||
|
||||
JsonObject dtu = doc.createNestedObject("dtu");
|
||||
JsonObject dtu = doc["dtu"].to<JsonObject>();
|
||||
dtu["serial"] = config.Dtu.Serial;
|
||||
dtu["poll_interval"] = config.Dtu.PollInterval;
|
||||
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
|
||||
@ -97,14 +93,14 @@ bool ConfigurationClass::write()
|
||||
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
|
||||
dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode;
|
||||
|
||||
JsonObject security = doc.createNestedObject("security");
|
||||
JsonObject security = doc["security"].to<JsonObject>();
|
||||
security["password"] = config.Security.Password;
|
||||
security["allow_readonly"] = config.Security.AllowReadonly;
|
||||
|
||||
JsonObject device = doc.createNestedObject("device");
|
||||
JsonObject device = doc["device"].to<JsonObject>();
|
||||
device["pinmapping"] = config.Dev_PinMapping;
|
||||
|
||||
JsonObject display = device.createNestedObject("display");
|
||||
JsonObject display = device["display"].to<JsonObject>();
|
||||
display["powersafe"] = config.Display.PowerSafe;
|
||||
display["screensaver"] = config.Display.ScreenSaver;
|
||||
display["rotation"] = config.Display.Rotation;
|
||||
@ -113,15 +109,15 @@ bool ConfigurationClass::write()
|
||||
display["diagram_duration"] = config.Display.Diagram.Duration;
|
||||
display["diagram_mode"] = config.Display.Diagram.Mode;
|
||||
|
||||
JsonArray leds = device.createNestedArray("led");
|
||||
JsonArray leds = device["led"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
|
||||
JsonObject led = leds.createNestedObject();
|
||||
JsonObject led = leds.add<JsonObject>();
|
||||
led["brightness"] = config.Led_Single[i].Brightness;
|
||||
}
|
||||
|
||||
JsonArray inverters = doc.createNestedArray("inverters");
|
||||
JsonArray inverters = doc["inverters"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
JsonObject inv = inverters.createNestedObject();
|
||||
JsonObject inv = inverters.add<JsonObject>();
|
||||
inv["serial"] = config.Inverter[i].Serial;
|
||||
inv["name"] = config.Inverter[i].Name;
|
||||
inv["order"] = config.Inverter[i].Order;
|
||||
@ -134,15 +130,19 @@ bool ConfigurationClass::write()
|
||||
inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight;
|
||||
inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection;
|
||||
|
||||
JsonArray channel = inv.createNestedArray("channel");
|
||||
JsonArray channel = inv["channel"].to<JsonArray>();
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
JsonObject chanData = channel.createNestedObject();
|
||||
JsonObject chanData = channel.add<JsonObject>();
|
||||
chanData["name"] = config.Inverter[i].channel[c].Name;
|
||||
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
|
||||
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serialize JSON to file
|
||||
if (serializeJson(doc, f) == 0) {
|
||||
MessageOutput.println("Failed to write file");
|
||||
@ -157,11 +157,7 @@ bool ConfigurationClass::read()
|
||||
{
|
||||
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
|
||||
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return false;
|
||||
}
|
||||
JsonDocument doc;
|
||||
|
||||
// Deserialize the JSON document
|
||||
const DeserializationError error = deserializeJson(doc, f);
|
||||
@ -169,6 +165,10 @@ bool ConfigurationClass::read()
|
||||
MessageOutput.println("Failed to read file, using default configuration");
|
||||
}
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonObject cfg = doc["cfg"];
|
||||
config.Cfg.Version = cfg["version"] | CONFIG_VERSION;
|
||||
config.Cfg.SaveCount = cfg["save_count"] | 0;
|
||||
@ -324,11 +324,7 @@ void ConfigurationClass::migrate()
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument doc;
|
||||
|
||||
// Deserialize the JSON document
|
||||
const DeserializationError error = deserializeJson(doc, f);
|
||||
@ -337,6 +333,10 @@ void ConfigurationClass::migrate()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Cfg.Version < 0x00011700) {
|
||||
JsonArray inverters = doc["inverters"];
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
@ -372,6 +372,12 @@ void ConfigurationClass::migrate()
|
||||
config.Dtu.Cmt.Frequency *= 1000;
|
||||
}
|
||||
|
||||
if (config.Cfg.Version < 0x00011c00) {
|
||||
if (!strcmp(config.Ntp.Server, NTP_SERVER_OLD)) {
|
||||
strlcpy(config.Ntp.Server, NTP_SERVER, sizeof(config.Ntp.Server));
|
||||
}
|
||||
}
|
||||
|
||||
f.close();
|
||||
|
||||
config.Cfg.Version = CONFIG_VERSION;
|
||||
@ -406,4 +412,26 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial)
|
||||
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;
|
||||
|
||||
@ -29,11 +29,16 @@ const uint8_t languages[] = {
|
||||
};
|
||||
|
||||
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_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_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_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" };
|
||||
|
||||
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
|
||||
dispX += offset;
|
||||
}
|
||||
|
||||
if (dispX > _display->getDisplayWidth()) {
|
||||
dispX = 0;
|
||||
}
|
||||
_display->drawStr(dispX, _lineOffsets[line], text);
|
||||
}
|
||||
|
||||
@ -237,15 +246,20 @@ void DisplayGraphicClass::loop()
|
||||
//<=======================
|
||||
|
||||
if (showText) {
|
||||
//=====> Today & Total Production =======
|
||||
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled());
|
||||
// Daily production
|
||||
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);
|
||||
|
||||
const float watts = Datastore.getTotalAcYieldTotalEnabled();
|
||||
auto const format = (watts >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
|
||||
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], watts);
|
||||
// Total production
|
||||
const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
|
||||
auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh;
|
||||
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal);
|
||||
printText(_fmtText, 2);
|
||||
//<=======================
|
||||
|
||||
//=====> IP or Date-Time ========
|
||||
// Change every 3 seconds
|
||||
|
||||
@ -51,9 +51,9 @@ void InverterSettingsClass::init(Scheduler& scheduler)
|
||||
|
||||
if (PinMapping.isValidCmt2300Config()) {
|
||||
Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3);
|
||||
MessageOutput.println(F(" Setting country mode... "));
|
||||
MessageOutput.println(" Setting country mode... ");
|
||||
Hoymiles.getRadioCmt()->setCountryMode(static_cast<CountryModeId_t>(config.Dtu.Cmt.CountryMode));
|
||||
MessageOutput.println(F(" Setting CMT target frequency... "));
|
||||
MessageOutput.println(" Setting CMT target frequency... ");
|
||||
Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency);
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "NetworkSettings.h"
|
||||
#include "Utils.h"
|
||||
#include "defaults.h"
|
||||
#include "__compiled_constants.h"
|
||||
|
||||
MqttHandleHassClass MqttHandleHass;
|
||||
|
||||
@ -137,10 +138,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
|
||||
name = "CH" + chanNum + " " + fieldName;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = name;
|
||||
root["stat_t"] = stateTopic;
|
||||
@ -163,6 +161,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
|
||||
root["stat_cla"] = stateCls;
|
||||
}
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
@ -185,10 +187,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
|
||||
|
||||
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = caption;
|
||||
root["uniq_id"] = serial + "_" + buttonId;
|
||||
@ -204,6 +203,10 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
|
||||
|
||||
createInverterInfo(root, inv);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
@ -227,10 +230,7 @@ void MqttHandleHassClass::publishInverterNumber(
|
||||
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic;
|
||||
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = caption;
|
||||
root["uniq_id"] = serial + "_" + buttonId;
|
||||
@ -246,6 +246,10 @@ void MqttHandleHassClass::publishInverterNumber(
|
||||
|
||||
createInverterInfo(root, inv);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
@ -265,10 +269,7 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
|
||||
|
||||
const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = caption;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
@ -278,6 +279,10 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
|
||||
|
||||
createInverterInfo(root, inv);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
@ -293,10 +298,7 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
|
||||
topic = id;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = name;
|
||||
root["uniq_id"] = getDtuUniqueId() + "_" + id;
|
||||
@ -322,6 +324,10 @@ void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_
|
||||
|
||||
createDtuInfo(root);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
|
||||
serializeJson(root, buffer);
|
||||
@ -339,10 +345,7 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
|
||||
topic = String("dtu/") + "/" + id;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
JsonDocument root;
|
||||
|
||||
root["name"] = name;
|
||||
root["uniq_id"] = getDtuUniqueId() + "_" + id;
|
||||
@ -359,13 +362,17 @@ void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* d
|
||||
|
||||
createDtuInfo(root);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config";
|
||||
serializeJson(root, buffer);
|
||||
publish(configTopic, buffer);
|
||||
}
|
||||
|
||||
void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::shared_ptr<InverterAbstract> inv)
|
||||
void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr<InverterAbstract> inv)
|
||||
{
|
||||
createDeviceInfo(
|
||||
root,
|
||||
@ -374,11 +381,11 @@ void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::sha
|
||||
getDtuUrl(),
|
||||
"OpenDTU",
|
||||
inv->typeName(),
|
||||
AUTO_GIT_HASH,
|
||||
__COMPILED_GIT_HASH__,
|
||||
getDtuUniqueId());
|
||||
}
|
||||
|
||||
void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root)
|
||||
void MqttHandleHassClass::createDtuInfo(JsonDocument& root)
|
||||
{
|
||||
createDeviceInfo(
|
||||
root,
|
||||
@ -387,16 +394,16 @@ void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root)
|
||||
getDtuUrl(),
|
||||
"OpenDTU",
|
||||
"OpenDTU",
|
||||
AUTO_GIT_HASH);
|
||||
__COMPILED_GIT_HASH__);
|
||||
}
|
||||
|
||||
void MqttHandleHassClass::createDeviceInfo(
|
||||
DynamicJsonDocument& root,
|
||||
JsonDocument& root,
|
||||
const String& name, const String& identifiers, const String& configuration_url,
|
||||
const String& manufacturer, const String& model, const String& sw_version,
|
||||
const String& via_device)
|
||||
{
|
||||
auto object = root.createNestedObject("dev");
|
||||
auto object = root["dev"].to<JsonObject>();
|
||||
|
||||
object["name"] = name;
|
||||
object["ids"] = identifiers;
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include "defaults.h"
|
||||
#include <ESPmDNS.h>
|
||||
#include <ETH.h>
|
||||
#include "__compiled_constants.h"
|
||||
|
||||
NetworkSettingsClass::NetworkSettingsClass()
|
||||
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this))
|
||||
@ -136,7 +137,7 @@ void NetworkSettingsClass::handleMDNS()
|
||||
|
||||
MDNS.addService("http", "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");
|
||||
} else {
|
||||
|
||||
@ -8,8 +8,6 @@
|
||||
#include <LittleFS.h>
|
||||
#include <string.h>
|
||||
|
||||
#define JSON_BUFFER_SIZE 6144
|
||||
|
||||
#ifndef DISPLAY_TYPE
|
||||
#define DISPLAY_TYPE 0U
|
||||
#endif
|
||||
@ -141,7 +139,7 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
return false;
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
JsonDocument doc;
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, f);
|
||||
if (error) {
|
||||
@ -216,4 +214,4 @@ bool PinMappingClass::isValidCmt2300Config() const
|
||||
bool PinMappingClass::isValidEthConfig() const
|
||||
{
|
||||
return _pinMapping.eth_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,9 +69,9 @@ void Utils::restartDtu()
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line)
|
||||
bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line)
|
||||
{
|
||||
if (doc.capacity() == 0) {
|
||||
if (doc.overflowed()) {
|
||||
MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
#include "WebApi.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "defaults.h"
|
||||
#include <AsyncJson.h>
|
||||
|
||||
@ -85,4 +86,58 @@ void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const
|
||||
}
|
||||
}
|
||||
|
||||
bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document)
|
||||
{
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return false;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
const DeserializationError error = deserializeJson(json_document, json);
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t WebApiClass::parseSerialFromRequest(AsyncWebServerRequest* request, String param_name)
|
||||
{
|
||||
if (request->hasParam(param_name)) {
|
||||
String s = request->getParam(param_name)->value();
|
||||
return strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line)
|
||||
{
|
||||
bool ret_val = true;
|
||||
if (response->overflowed()) {
|
||||
auto& root = response->getRoot();
|
||||
|
||||
root.clear();
|
||||
root["message"] = String("500 Internal Server Error: ") + function + ", " + line;
|
||||
root["code"] = WebApiError::GenericInternalServerError;
|
||||
root["type"] = "danger";
|
||||
response->setCode(500);
|
||||
MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line);
|
||||
ret_val = false;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
WebApiClass WebApi;
|
||||
|
||||
@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
||||
requestFile = name;
|
||||
} else {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,51 +54,24 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("delete"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["delete"].as<bool>() == false) {
|
||||
retMsg["message"] = "Not deleted anything!";
|
||||
retMsg["code"] = WebApiError::ConfigNotDeleted;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,8 +79,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Configuration resettet. Rebooting now...";
|
||||
retMsg["code"] = WebApiError::ConfigSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
Utils::removeAllFiles();
|
||||
Utils::restartDtu();
|
||||
@ -120,7 +93,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
auto data = root.createNestedArray("configs");
|
||||
auto data = root["configs"].to<JsonArray>();
|
||||
|
||||
File rootfs = LittleFS.open("/");
|
||||
File file = rootfs.openNextFile();
|
||||
@ -128,15 +101,14 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
|
||||
if (file.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
JsonObject obj = data.createNestedObject();
|
||||
JsonObject obj = data.add<JsonObject>();
|
||||
obj["name"] = String(file.name());
|
||||
|
||||
file = rootfs.openNextFile();
|
||||
}
|
||||
file.close();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
|
||||
|
||||
@ -26,15 +26,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
|
||||
auto curPin = root.createNestedObject("curPin");
|
||||
auto curPin = root["curPin"].to<JsonObject>();
|
||||
curPin["name"] = config.Dev_PinMapping;
|
||||
|
||||
auto nrfPinObj = curPin.createNestedObject("nrf24");
|
||||
auto nrfPinObj = curPin["nrf24"].to<JsonObject>();
|
||||
nrfPinObj["clk"] = pin.nrf24_clk;
|
||||
nrfPinObj["cs"] = pin.nrf24_cs;
|
||||
nrfPinObj["en"] = pin.nrf24_en;
|
||||
@ -42,7 +42,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
nrfPinObj["miso"] = pin.nrf24_miso;
|
||||
nrfPinObj["mosi"] = pin.nrf24_mosi;
|
||||
|
||||
auto cmtPinObj = curPin.createNestedObject("cmt");
|
||||
auto cmtPinObj = curPin["cmt"].to<JsonObject>();
|
||||
cmtPinObj["clk"] = pin.cmt_clk;
|
||||
cmtPinObj["cs"] = pin.cmt_cs;
|
||||
cmtPinObj["fcs"] = pin.cmt_fcs;
|
||||
@ -50,7 +50,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
cmtPinObj["gpio2"] = pin.cmt_gpio2;
|
||||
cmtPinObj["gpio3"] = pin.cmt_gpio3;
|
||||
|
||||
auto ethPinObj = curPin.createNestedObject("eth");
|
||||
auto ethPinObj = curPin["eth"].to<JsonObject>();
|
||||
ethPinObj["enabled"] = pin.eth_enabled;
|
||||
ethPinObj["phy_addr"] = pin.eth_phy_addr;
|
||||
ethPinObj["power"] = pin.eth_power;
|
||||
@ -59,19 +59,19 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
ethPinObj["type"] = pin.eth_type;
|
||||
ethPinObj["clk_mode"] = pin.eth_clk_mode;
|
||||
|
||||
auto displayPinObj = curPin.createNestedObject("display");
|
||||
auto displayPinObj = curPin["display"].to<JsonObject>();
|
||||
displayPinObj["type"] = pin.display_type;
|
||||
displayPinObj["data"] = pin.display_data;
|
||||
displayPinObj["clk"] = pin.display_clk;
|
||||
displayPinObj["cs"] = pin.display_cs;
|
||||
displayPinObj["reset"] = pin.display_reset;
|
||||
|
||||
auto ledPinObj = curPin.createNestedObject("led");
|
||||
auto ledPinObj = curPin["led"].to<JsonObject>();
|
||||
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
|
||||
ledPinObj["led" + String(i)] = pin.led[i];
|
||||
}
|
||||
|
||||
auto display = root.createNestedObject("display");
|
||||
auto display = root["display"].to<JsonObject>();
|
||||
display["rotation"] = config.Display.Rotation;
|
||||
display["power_safe"] = config.Display.PowerSafe;
|
||||
display["screensaver"] = config.Display.ScreenSaver;
|
||||
@ -80,14 +80,13 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
display["diagramduration"] = config.Display.Diagram.Duration;
|
||||
display["diagrammode"] = config.Display.Diagram.Mode;
|
||||
|
||||
auto leds = root.createNestedArray("led");
|
||||
auto leds = root["led"].to<JsonArray>();
|
||||
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
|
||||
auto led = leds.createNestedObject();
|
||||
auto led = leds.add<JsonObject>();
|
||||
led["brightness"] = config.Led_Single[i].Brightness;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
@ -96,45 +95,19 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("curPin")
|
||||
|| root.containsKey("display"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -142,8 +115,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::HardwarePinMappingLength;
|
||||
retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,8 +146,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
if (performRestart) {
|
||||
Utils::restartDtu();
|
||||
|
||||
@ -23,13 +23,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
auto serial = WebApi.parseSerialFromRequest(request);
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv != nullptr) {
|
||||
@ -43,6 +37,5 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
|
||||
root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr();
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -62,10 +62,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
root["cmt_country"] = config.Dtu.Cmt.CountryMode;
|
||||
root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth();
|
||||
|
||||
auto data = root.createNestedArray("country_def");
|
||||
auto data = root["country_def"].to<JsonArray>();
|
||||
auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList();
|
||||
for (const auto& definition : countryDefs) {
|
||||
auto obj = data.createNestedObject();
|
||||
auto obj = data.add<JsonObject>();
|
||||
obj["freq_default"] = definition.definition.Freq_Default;
|
||||
obj["freq_min"] = definition.definition.Freq_Min;
|
||||
obj["freq_max"] = definition.definition.Freq_Max;
|
||||
@ -73,8 +73,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
obj["freq_legal_max"] = definition.definition.Freq_Legal_Max;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
@ -84,37 +83,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("pollinterval")
|
||||
@ -124,48 +98,45 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("cmt_country"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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["code"] = WebApiError::DtuSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["pollinterval"].as<uint32_t>() == 0) {
|
||||
retMsg["message"] = "Poll interval must be greater zero!";
|
||||
retMsg["code"] = WebApiError::DtuPollZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["nrf_palevel"].as<uint8_t>() > 3) {
|
||||
retMsg["message"] = "Invalid power level setting!";
|
||||
retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["cmt_palevel"].as<int8_t>() < -10 || root["cmt_palevel"].as<int8_t>() > 20) {
|
||||
retMsg["message"] = "Invalid power level setting!";
|
||||
retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["cmt_country"].as<uint8_t>() >= CountryModeId_t::CountryModeId_Max) {
|
||||
retMsg["message"] = "Invalid country setting!";
|
||||
retMsg["code"] = WebApiError::DtuInvalidCmtCountry;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -178,15 +149,13 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::DtuInvalidCmtFrequency;
|
||||
retMsg["param"]["min"] = FrequencyDefinition.Freq_Min;
|
||||
retMsg["param"]["max"] = FrequencyDefinition.Freq_Max;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
config.Dtu.Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
config.Dtu.Serial = serial;
|
||||
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
|
||||
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
|
||||
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
|
||||
@ -195,8 +164,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
_applyDataTask.enable();
|
||||
_applyDataTask.restart();
|
||||
}
|
||||
|
||||
@ -20,14 +20,9 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
auto serial = WebApi.parseSerialFromRequest(request);
|
||||
|
||||
AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN;
|
||||
if (request->hasParam("locale")) {
|
||||
@ -47,10 +42,10 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
uint8_t logEntryCount = inv->EventLog()->getEntryCount();
|
||||
|
||||
root["count"] = logEntryCount;
|
||||
JsonArray eventsArray = root.createNestedArray("events");
|
||||
JsonArray eventsArray = root["events"].to<JsonArray>();
|
||||
|
||||
for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) {
|
||||
JsonObject eventsObject = eventsArray.createNestedObject();
|
||||
JsonObject eventsObject = eventsArray.add<JsonObject>();
|
||||
|
||||
AlarmLogEntry_t entry;
|
||||
inv->EventLog()->getLogEntry(logEntry, entry, locale);
|
||||
@ -62,6 +57,5 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
}
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -21,32 +21,26 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
auto serial = WebApi.parseSerialFromRequest(request);
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv != nullptr) {
|
||||
root["name"] = inv->GridProfile()->getProfileName();
|
||||
root["version"] = inv->GridProfile()->getProfileVersion();
|
||||
|
||||
auto jsonSections = root.createNestedArray("sections");
|
||||
auto jsonSections = root["sections"].to<JsonArray>();
|
||||
auto profSections = inv->GridProfile()->getProfile();
|
||||
|
||||
for (auto &profSection : profSections) {
|
||||
auto jsonSection = jsonSections.createNestedObject();
|
||||
auto jsonSection = jsonSections.add<JsonObject>();
|
||||
jsonSection["name"] = profSection.SectionName;
|
||||
|
||||
auto jsonItems = jsonSection.createNestedArray("items");
|
||||
auto jsonItems = jsonSection["items"].to<JsonArray>();
|
||||
|
||||
for (auto &profItem : profSection.items) {
|
||||
auto jsonItem = jsonItems.createNestedObject();
|
||||
auto jsonItem = jsonItems.add<JsonObject>();
|
||||
|
||||
jsonItem["n"] = profItem.Name;
|
||||
jsonItem["u"] = profItem.Unit;
|
||||
@ -55,8 +49,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request)
|
||||
}
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request)
|
||||
@ -65,24 +58,17 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
auto serial = WebApi.parseSerialFromRequest(request);
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv != nullptr) {
|
||||
auto raw = root.createNestedArray("raw");
|
||||
auto raw = root["raw"].to<JsonArray>();
|
||||
auto data = inv->GridProfile()->getRawData();
|
||||
|
||||
copyArray(&data[0], data.size(), raw);
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -29,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
JsonArray data = root.createNestedArray("inverter");
|
||||
JsonArray data = root["inverter"].to<JsonArray>();
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial > 0) {
|
||||
JsonObject obj = data.createNestedObject();
|
||||
JsonObject obj = data.add<JsonObject>();
|
||||
obj["id"] = i;
|
||||
obj["name"] = String(config.Inverter[i].Name);
|
||||
obj["order"] = config.Inverter[i].Order;
|
||||
@ -67,9 +67,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size();
|
||||
}
|
||||
|
||||
JsonArray channel = obj.createNestedArray("channel");
|
||||
JsonArray channel = obj["channel"].to<JsonArray>();
|
||||
for (uint8_t c = 0; c < max_channels; c++) {
|
||||
JsonObject chanData = channel.createNestedObject();
|
||||
JsonObject chanData = channel.add<JsonObject>();
|
||||
chanData["name"] = config.Inverter[i].channel[c].Name;
|
||||
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
|
||||
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
|
||||
@ -77,8 +77,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
}
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
@ -88,52 +87,28 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("name"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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["code"] = WebApiError::InverterSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
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["code"] = WebApiError::InverterNameLength;
|
||||
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -152,20 +126,18 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!";
|
||||
retMsg["code"] = WebApiError::InverterCount;
|
||||
retMsg["param"]["max"] = INV_MAX_COUNT;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial);
|
||||
|
||||
@ -185,59 +157,34 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg["message"] = "Invalid ID specified!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidId;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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["code"] = WebApiError::InverterSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
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["code"] = WebApiError::InverterNameLength;
|
||||
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -254,14 +200,13 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) {
|
||||
retMsg["message"] = "Invalid amount of max channel setting given!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidMaxChannel;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial);
|
||||
|
||||
@ -327,51 +271,24 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("id"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg["message"] = "Invalid ID specified!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidId;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -380,13 +297,11 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
|
||||
Hoymiles.removeInverterBySerial(inverter.Serial);
|
||||
|
||||
inverter.Serial = 0;
|
||||
strncpy(inverter.Name, "", sizeof(inverter.Name));
|
||||
Configuration.deleteInverterById(inverter_id);
|
||||
|
||||
WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
MqttHandleHass.forceUpdate();
|
||||
}
|
||||
@ -398,43 +313,17 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("order"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -452,6 +341,5 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -47,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
|
||||
root[serial]["limit_set_status"] = limitStatus;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
@ -58,53 +57,29 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("limit_value")
|
||||
&& root.containsKey("limit_type"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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["code"] = WebApiError::LimitSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -112,8 +87,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = MAX_INVERTER_LIMIT;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,12 +98,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
|
||||
retMsg["message"] = "Invalid type specified!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
uint16_t limit = root["limit_value"].as<uint16_t>();
|
||||
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
|
||||
|
||||
@ -137,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
if (inv == nullptr) {
|
||||
retMsg["message"] = "Invalid inverter specified!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidInverter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -148,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -22,44 +22,18 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("reboot"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -68,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Reboot triggered!";
|
||||
retMsg["code"] = WebApiError::MaintenanceRebootTriggered;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
Utils::restartDtu();
|
||||
} else {
|
||||
retMsg["message"] = "Reboot cancled!";
|
||||
retMsg["code"] = WebApiError::MaintenanceRebootCancled;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
@ -50,8 +50,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic;
|
||||
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
@ -60,7 +59,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
@ -88,8 +87,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic;
|
||||
root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
@ -98,38 +96,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("mqtt_enabled")
|
||||
&& root.containsKey("mqtt_hostname")
|
||||
@ -155,8 +128,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("mqtt_hass_individualpanels"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -165,8 +137,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::MqttHostnameLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -174,48 +145,42 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttUsernameLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
if (root["mqtt_password"].as<String>().length() > MQTT_MAX_PASSWORD_STRLEN) {
|
||||
retMsg["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttPasswordLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
if (root["mqtt_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["mqtt_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "Topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root["mqtt_topic"].as<String>().endsWith("/")) {
|
||||
retMsg["message"] = "Topic must end with a slash (/)!";
|
||||
retMsg["code"] = WebApiError::MqttTopicTrailingSlash;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["mqtt_port"].as<uint>() == 0 || root["mqtt_port"].as<uint>() > 65535) {
|
||||
retMsg["message"] = "Port must be a number between 1 and 65535!";
|
||||
retMsg["code"] = WebApiError::MqttPort;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -225,8 +190,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttCertificateLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -234,16 +198,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["mqtt_lwt_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "LWT topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -251,8 +213,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtOnlineLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -260,8 +221,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtOfflineLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -269,8 +229,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!";
|
||||
retMsg["code"] = WebApiError::MqttLwtQos;
|
||||
retMsg["param"]["max"] = 2;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -279,8 +238,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::MqttPublishInterval;
|
||||
retMsg["param"]["min"] = 5;
|
||||
retMsg["param"]["max"] = 65535;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -289,16 +247,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttHassTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["mqtt_hass_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "Hass topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttHassTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -331,8 +287,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
MqttSettings.performReconnect();
|
||||
MqttHandleHass.forceUpdate();
|
||||
|
||||
@ -46,8 +46,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
|
||||
root["ap_mac"] = WiFi.softAPmacAddress();
|
||||
root["ap_stationnum"] = WiFi.softAPgetStationNum();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
@ -72,8 +71,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
root["aptimeout"] = config.WiFi.ApTimeout;
|
||||
root["mdnsenabled"] = config.Mdns.Enabled;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
@ -83,37 +81,12 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("ssid")
|
||||
&& root.containsKey("password")
|
||||
@ -127,8 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("aptimeout"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,68 +108,59 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
if (!ipaddress.fromString(root["ipaddress"].as<String>())) {
|
||||
retMsg["message"] = "IP address is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkIpInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
IPAddress netmask;
|
||||
if (!netmask.fromString(root["netmask"].as<String>())) {
|
||||
retMsg["message"] = "Netmask is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkNetmaskInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
IPAddress gateway;
|
||||
if (!gateway.fromString(root["gateway"].as<String>())) {
|
||||
retMsg["message"] = "Gateway is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkGatewayInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
IPAddress dns1;
|
||||
if (!dns1.fromString(root["dns1"].as<String>())) {
|
||||
retMsg["message"] = "DNS Server IP 1 is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkDns1Invalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
IPAddress dns2;
|
||||
if (!dns2.fromString(root["dns2"].as<String>())) {
|
||||
retMsg["message"] = "DNS Server IP 2 is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkDns2Invalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["hostname"].as<String>().length() == 0 || root["hostname"].as<String>().length() > WIFI_MAX_HOSTNAME_STRLEN) {
|
||||
retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
|
||||
if (root["ssid"].as<String>().length() == 0 || root["ssid"].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
|
||||
retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (root["password"].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
|
||||
retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
if (root["aptimeout"].as<uint>() > 99999) {
|
||||
retMsg["message"] = "ApTimeout must be a number between 0 and 99999!";
|
||||
retMsg["code"] = WebApiError::NetworkApTimeoutInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -235,8 +198,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
NetworkSettings.enableAdminMode();
|
||||
NetworkSettings.applyConfig();
|
||||
|
||||
@ -63,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
|
||||
root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable();
|
||||
root["sun_isDayPeriod"] = SunPosition.isDayPeriod();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
@ -84,8 +83,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
root["latitude"] = config.Ntp.Latitude;
|
||||
root["sunsettype"] = config.Ntp.SunsetType;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
@ -95,37 +93,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("ntp_server")
|
||||
&& root.containsKey("ntp_timezone")
|
||||
@ -134,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("sunsettype"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -143,8 +115,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpServerLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -152,8 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpTimezoneLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -161,8 +131,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -176,8 +145,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
NtpSettings.setServer();
|
||||
NtpSettings.setTimezone();
|
||||
@ -208,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
|
||||
root["minute"] = timeinfo.tm_min;
|
||||
root["second"] = timeinfo.tm_sec;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
@ -219,37 +186,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("year")
|
||||
&& root.containsKey("month")
|
||||
@ -259,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("second"))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -269,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpYearInvalid;
|
||||
retMsg["param"]["min"] = 2022;
|
||||
retMsg["param"]["max"] = 2100;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -279,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpMonthInvalid;
|
||||
retMsg["param"]["min"] = 1;
|
||||
retMsg["param"]["max"] = 12;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -289,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpDayInvalid;
|
||||
retMsg["param"]["min"] = 1;
|
||||
retMsg["param"]["max"] = 31;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpHourInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 23;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -309,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpMinuteInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 59;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -319,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["code"] = WebApiError::NtpSecondInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 59;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -341,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Time updated!";
|
||||
retMsg["code"] = WebApiError::NtpTimeUpdated;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -40,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
|
||||
root[inv->serialString()]["power_set_status"] = limitStatus;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
@ -51,63 +50,37 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& (root.containsKey("power")
|
||||
|| root.containsKey("restart")))) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
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["code"] = WebApiError::PowerSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
if (inv == nullptr) {
|
||||
retMsg["message"] = "Invalid inverter specified!";
|
||||
retMsg["code"] = WebApiError::PowerInvalidInverter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "NetworkSettings.h"
|
||||
#include "WebApi.h"
|
||||
#include <Hoymiles.h>
|
||||
#include "__compiled_constants.h"
|
||||
|
||||
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("# TYPE opendtu_build gauge\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("# TYPE opendtu_platform gauge\n");
|
||||
@ -142,7 +143,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
|
||||
return;
|
||||
}
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
const auto& config = Configuration.getInverterConfig(inv->serial());
|
||||
|
||||
const bool printHelp = (idx == 0 && channel == 0);
|
||||
if (printHelp) {
|
||||
@ -154,7 +155,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
|
||||
idx,
|
||||
inv->name(),
|
||||
channel,
|
||||
config.Inverter[idx].channel[channel].Name);
|
||||
config->channel[channel].Name);
|
||||
|
||||
if (printHelp) {
|
||||
stream->print("# HELP opendtu_MaxPower panel maximum output power\n");
|
||||
@ -165,7 +166,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
|
||||
idx,
|
||||
inv->name(),
|
||||
channel,
|
||||
config.Inverter[idx].channel[channel].MaxChannelPower);
|
||||
config->channel[channel].MaxChannelPower);
|
||||
|
||||
if (printHelp) {
|
||||
stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n");
|
||||
@ -176,5 +177,5 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
|
||||
idx,
|
||||
inv->name(),
|
||||
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["allow_readonly"] = config.Security.AllowReadonly;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
@ -42,44 +41,18 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonDocument root;
|
||||
if (!WebApi.parseRequestData(request, response, root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& retMsg = response->getRoot();
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
const DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.containsKey("password")
|
||||
&& root.containsKey("allow_readonly")) {
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -87,8 +60,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::SecurityPasswordLength;
|
||||
retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -98,8 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
|
||||
@ -114,6 +85,5 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
|
||||
retMsg["message"] = "Authentication successful!";
|
||||
retMsg["code"] = WebApiError::SecurityAuthSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -11,10 +11,7 @@
|
||||
#include <Hoymiles.h>
|
||||
#include <LittleFS.h>
|
||||
#include <ResetReason.h>
|
||||
|
||||
#ifndef AUTO_GIT_HASH
|
||||
#define AUTO_GIT_HASH ""
|
||||
#endif
|
||||
#include "__compiled_constants.h"
|
||||
|
||||
void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler)
|
||||
{
|
||||
@ -64,7 +61,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
|
||||
char version[16];
|
||||
snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
|
||||
root["config_version"] = version;
|
||||
root["git_hash"] = AUTO_GIT_HASH;
|
||||
root["git_hash"] = __COMPILED_GIT_HASH__;
|
||||
root["pioenv"] = PIOENV;
|
||||
|
||||
root["uptime"] = esp_timer_get_time() / 1000000;
|
||||
@ -76,6 +73,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
|
||||
root["cmt_configured"] = PinMapping.isValidCmt2300Config();
|
||||
root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
}
|
||||
|
||||
@ -73,19 +73,20 @@ void WebApiWsLiveClass::sendDataTaskCb()
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
DynamicJsonDocument root(4096);
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
continue;
|
||||
}
|
||||
JsonDocument root;
|
||||
JsonVariant var = root;
|
||||
|
||||
auto invArray = var.createNestedArray("inverters");
|
||||
auto invObject = invArray.createNestedObject();
|
||||
auto invArray = var["inverters"].to<JsonArray>();
|
||||
auto invObject = invArray.add<JsonObject>();
|
||||
|
||||
generateCommonJsonResponse(var);
|
||||
generateInverterCommonJsonResponse(invObject, inv);
|
||||
generateInverterChannelJsonResponse(invObject, inv);
|
||||
|
||||
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String buffer;
|
||||
serializeJson(root, buffer);
|
||||
|
||||
@ -101,12 +102,12 @@ void WebApiWsLiveClass::sendDataTaskCb()
|
||||
|
||||
void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root)
|
||||
{
|
||||
JsonObject totalObj = root.createNestedObject("total");
|
||||
auto totalObj = root["total"].to<JsonObject>();
|
||||
addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits());
|
||||
addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits());
|
||||
addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits());
|
||||
|
||||
JsonObject hintObj = root.createNestedObject("hints");
|
||||
JsonObject hintObj = root["hints"].to<JsonObject>();
|
||||
struct tm timeinfo;
|
||||
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
|
||||
hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected()));
|
||||
@ -144,7 +145,7 @@ void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, st
|
||||
|
||||
// Loop all channels
|
||||
for (auto& t : inv->Statistics()->getChannelTypes()) {
|
||||
JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t));
|
||||
auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to<JsonObject>();
|
||||
for (auto& c : inv->Statistics()->getChannelsByType(t)) {
|
||||
if (t == TYPE_DC) {
|
||||
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
|
||||
@ -221,21 +222,15 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
|
||||
try {
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096);
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
|
||||
JsonArray invArray = root.createNestedArray("inverters");
|
||||
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
auto invArray = root["inverters"].to<JsonArray>();
|
||||
auto serial = WebApi.parseSerialFromRequest(request);
|
||||
|
||||
if (serial > 0) {
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
if (inv != nullptr) {
|
||||
JsonObject invObject = invArray.createNestedObject();
|
||||
JsonObject invObject = invArray.add<JsonObject>();
|
||||
generateInverterCommonJsonResponse(invObject, inv);
|
||||
generateInverterChannelJsonResponse(invObject, inv);
|
||||
}
|
||||
@ -247,15 +242,14 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonObject invObject = invArray.createNestedObject();
|
||||
JsonObject invObject = invArray.add<JsonObject>();
|
||||
generateInverterCommonJsonResponse(invObject, inv);
|
||||
}
|
||||
}
|
||||
|
||||
generateCommonJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
|
||||
} catch (const std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
@ -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,41 +9,40 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons-vue": "^1.11.3",
|
||||
"mitt": "^3.0.1",
|
||||
"sortablejs": "^1.15.2",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.4.19",
|
||||
"vue-i18n": "^9.9.1",
|
||||
"vue-router": "^4.2.5"
|
||||
"vue": "^3.4.26",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^2.0.0",
|
||||
"@rushstack/eslint-patch": "^1.7.2",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^4.0.0",
|
||||
"@tsconfig/node18": "^18.2.4",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/node": "^20.12.10",
|
||||
"@types/pulltorefreshjs": "^0.1.7",
|
||||
"@types/sortablejs": "^1.15.7",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/spark-md5": "^3.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",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.21.1",
|
||||
"eslint": "^9.2.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pulltorefreshjs": "^0.1.22",
|
||||
"sass": "^1.71.0",
|
||||
"terser": "^5.27.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.3",
|
||||
"sass": "^1.76.0",
|
||||
"terser": "^5.31.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-css-injected-by-js": "^3.4.0",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"vite-plugin-css-injected-by-js": "^3.5.1",
|
||||
"vue-tsc": "^2.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@
|
||||
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo":"AST4",
|
||||
"America/Sao_Paulo":"<-03>3",
|
||||
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||
"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0",
|
||||
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy":"AST4",
|
||||
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
|
||||
@ -200,7 +200,7 @@
|
||||
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey":"<+11>-11",
|
||||
"Antarctica/Casey":"<+08>-8",
|
||||
"Antarctica/Davis":"<+07>-7",
|
||||
"Antarctica/DumontDUrville":"<+10>-10",
|
||||
"Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
@ -210,10 +210,10 @@
|
||||
"Antarctica/Rothera":"<-03>3",
|
||||
"Antarctica/Syowa":"<+03>-3",
|
||||
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok":"<+06>-6",
|
||||
"Antarctica/Vostok":"<+05>-5",
|
||||
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden":"<+03>-3",
|
||||
"Asia/Almaty":"<+06>-6",
|
||||
"Asia/Almaty":"<+05>-5",
|
||||
"Asia/Amman":"<+03>-3",
|
||||
"Asia/Anadyr":"<+12>-12",
|
||||
"Asia/Aqtau":"<+05>-5",
|
||||
|
||||
@ -48,15 +48,14 @@ export default defineComponent({
|
||||
showReload: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
mounted() {
|
||||
var self = this;
|
||||
console.log("init");
|
||||
PullToRefresh.init({
|
||||
mainElement: 'body', // above which element?
|
||||
instructionsPullToRefresh: this.$t('base.Pull'),
|
||||
instructionsReleaseToRefresh: this.$t('base.Release'),
|
||||
instructionsRefreshing: this.$t('base.Refreshing'),
|
||||
onRefresh: function() {
|
||||
self.$emit('reload');
|
||||
onRefresh: () => {
|
||||
this.$emit('reload');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@ -52,7 +52,7 @@ export default defineComponent({
|
||||
_countDownTimeout = undefined;
|
||||
};
|
||||
|
||||
var countDown = ref();
|
||||
const countDown = ref();
|
||||
watch(() => props.modelValue, () => {
|
||||
countDown.value = parseCountDown(props.modelValue);
|
||||
});
|
||||
@ -116,4 +116,4 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -76,14 +76,14 @@ export default defineComponent({
|
||||
},
|
||||
productionYear() {
|
||||
return() => {
|
||||
return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014;
|
||||
return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014;
|
||||
}
|
||||
},
|
||||
productionWeek() {
|
||||
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>
|
||||
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
|
||||
<td v-if="modelAllowVersionInfo">
|
||||
<a :href="systemStatus.update_url" target="_blank" v-tooltip
|
||||
: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">
|
||||
<td>
|
||||
<div class="form-check form-check-inline form-switch">
|
||||
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
|
||||
<label class="form-check-label">
|
||||
<a v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined" :href="systemStatus.update_url" target="_blank" v-tooltip
|
||||
:title="$t('firmwareinfo.FirmwareUpdateHint')">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
@ -80,10 +83,10 @@ export default defineComponent({
|
||||
},
|
||||
computed: {
|
||||
modelAllowVersionInfo: {
|
||||
get(): any {
|
||||
get(): boolean {
|
||||
return !!this.allowVersionInfo;
|
||||
},
|
||||
set(value: any) {
|
||||
set(value: boolean) {
|
||||
this.$emit('update:allowVersionInfo', value);
|
||||
},
|
||||
},
|
||||
|
||||
@ -83,10 +83,12 @@ export default defineComponent({
|
||||
},
|
||||
computed: {
|
||||
model: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(): any {
|
||||
if (this.type === 'checkbox') return !!this.modelValue;
|
||||
return this.modelValue;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
set(value: any) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
@ -112,4 +114,4 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
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() {
|
||||
const easter = this.getEasterSunday(this.now.getFullYear());
|
||||
var easterStart = new Date(easter);
|
||||
var easterEnd = new Date(easter);
|
||||
const easterStart = new Date(easter);
|
||||
const easterEnd = new Date(easter);
|
||||
easterStart.setDate(easterStart.getDate() - 2);
|
||||
easterEnd.setDate(easterEnd.getDate() + 1);
|
||||
return this.now >= easterStart && this.now < easterEnd;
|
||||
@ -170,18 +170,18 @@ export default defineComponent({
|
||||
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");
|
||||
},
|
||||
getEasterSunday(year: number): Date {
|
||||
var f = Math.floor;
|
||||
var G = year % 19;
|
||||
var C = f(year / 100);
|
||||
var H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30;
|
||||
var I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11));
|
||||
var J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7;
|
||||
var L = I - J;
|
||||
var month = 3 + f((L + 40) / 44);
|
||||
var day = L + 28 - 31 * f(month / 4);
|
||||
const f = Math.floor;
|
||||
const G = year % 19;
|
||||
const C = f(year / 100);
|
||||
const H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30;
|
||||
const I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11));
|
||||
const J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7;
|
||||
const L = I - J;
|
||||
const month = 3 + f((L + 40) / 44);
|
||||
const day = L + 28 - 31 * f(month / 4);
|
||||
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -84,9 +84,11 @@ export default defineComponent({
|
||||
let comCur = 999999;
|
||||
|
||||
if (this.selectedPinAssignment && category in this.selectedPinAssignment) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
comSel = (this.selectedPinAssignment as any)[category][prop];
|
||||
}
|
||||
if (this.currentPinAssignment && category in this.currentPinAssignment) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
comCur = (this.currentPinAssignment as any)[category][prop];
|
||||
}
|
||||
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
"Release": "Loslassen zum Aktualisieren",
|
||||
"Close": "Schließen"
|
||||
},
|
||||
"Error": {
|
||||
"Oops": "Oops!"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Dunkel",
|
||||
"Light": "Hell",
|
||||
@ -618,5 +621,12 @@
|
||||
"Name": "Name",
|
||||
"ValueSelected": "Ausgewählt",
|
||||
"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",
|
||||
"Close": "Close"
|
||||
},
|
||||
"Error": {
|
||||
"Oops": "Oops!"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Dark",
|
||||
"Light": "Light",
|
||||
@ -619,5 +622,12 @@
|
||||
"Number": "Number",
|
||||
"ValueSelected": "Selected",
|
||||
"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",
|
||||
"Close": "Fermer"
|
||||
},
|
||||
"Error": {
|
||||
"Oops": "Oops!"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Sombre",
|
||||
"Light": "Clair",
|
||||
@ -618,5 +621,12 @@
|
||||
"Name": "Nom",
|
||||
"ValueSelected": "Sélectionné",
|
||||
"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 DeviceAdminView from '@/views/DeviceAdminView.vue'
|
||||
import DtuAdminView from '@/views/DtuAdminView.vue';
|
||||
import ErrorView from '@/views/ErrorView.vue';
|
||||
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import InverterAdminView from '@/views/InverterAdminView.vue';
|
||||
@ -32,6 +33,11 @@ const router = createRouter({
|
||||
name: 'Login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/error?status=:status&message=:message',
|
||||
name: 'Error',
|
||||
component: ErrorView
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
@ -115,4 +121,4 @@ const router = createRouter({
|
||||
]
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface DevInfoStatus {
|
||||
serial: number;
|
||||
serial: string;
|
||||
valid_data: boolean;
|
||||
fw_bootloader_version: number;
|
||||
fw_build_version: number;
|
||||
@ -8,4 +8,4 @@ export interface DevInfoStatus {
|
||||
hw_version: number;
|
||||
hw_model_name: string;
|
||||
max_power: number;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export interface InverterChannel {
|
||||
|
||||
export interface Inverter {
|
||||
id: string;
|
||||
serial: number;
|
||||
serial: string;
|
||||
name: string;
|
||||
type: string;
|
||||
order: number;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export interface LimitConfig {
|
||||
serial: number;
|
||||
serial: string;
|
||||
limit_value: number;
|
||||
limit_type: number;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export interface InverterStatistics {
|
||||
}
|
||||
|
||||
export interface Inverter {
|
||||
serial: number;
|
||||
serial: string;
|
||||
name: string;
|
||||
order: number;
|
||||
data_age: number;
|
||||
@ -53,4 +53,4 @@ export interface LiveData {
|
||||
inverters: Inverter[];
|
||||
total: Total;
|
||||
hints: Hints;
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export function isLoggedIn(): boolean {
|
||||
return (localStorage.getItem('user') != null);
|
||||
}
|
||||
|
||||
export function login(username: String, password: String) {
|
||||
export function login(username: string, password: string) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -65,7 +65,7 @@ export function login(username: String, password: String) {
|
||||
});
|
||||
}
|
||||
|
||||
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>, router: Router) {
|
||||
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>, router: Router, ignore_error: boolean = false) {
|
||||
return response.text().then(text => {
|
||||
const data = text && JSON.parse(text);
|
||||
if (!response.ok) {
|
||||
@ -74,9 +74,13 @@ export function handleResponse(response: Response, emitter: Emitter<Record<Event
|
||||
logout();
|
||||
emitter.emit("logged-out");
|
||||
router.push({ path: "/login", query: { returnUrl: router.currentRoute.value.fullPath } });
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const error = { message: (data && data.message) || response.statusText, status: response.status || 0 };
|
||||
if (!ignore_error) {
|
||||
router.push({ name: "Error", params: error });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@ -99,4 +103,4 @@ function handleAuthResponse(response: Response) {
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,8 +188,8 @@ export default defineComponent({
|
||||
fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() })
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
var file = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
const file = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = file;
|
||||
a.download = this.backupFileSelect;
|
||||
document.body.appendChild(a);
|
||||
|
||||
@ -1,154 +1,154 @@
|
||||
<template>
|
||||
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
|
||||
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
|
||||
v-model="isAutoScroll">
|
||||
<label class="form-check-label" for="autoScroll">
|
||||
{{ $t('console.EnableAutoScroll') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" :onClick="clearConsole">
|
||||
{{ $t('console.ClearConsole') }}</button>
|
||||
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
|
||||
{{ $t('console.CopyToClipboard') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
|
||||
</CardElement>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import { authUrl } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
CardElement,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataLoading: true,
|
||||
consoleBuffer: "",
|
||||
isAutoScroll: true,
|
||||
endWithNewline: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initSocket();
|
||||
this.dataLoading = false;
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
watch: {
|
||||
consoleBuffer() {
|
||||
if (this.isAutoScroll) {
|
||||
let textarea = this.$el.querySelector("#console");
|
||||
setTimeout(() => {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initSocket() {
|
||||
console.log("Starting connection to WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/console`;
|
||||
|
||||
this.closeSocket();
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
|
||||
let outstr = new String(event.data);
|
||||
let removedNewline = false;
|
||||
if (outstr.endsWith('\n')) {
|
||||
outstr = outstr.substring(0, outstr.length - 1);
|
||||
removedNewline = true;
|
||||
}
|
||||
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
|
||||
this.endWithNewline = removedNewline;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 5 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch {
|
||||
// continue regardless of error
|
||||
}
|
||||
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
},
|
||||
getOutDate(): String {
|
||||
const u = new Date();
|
||||
return ('0' + u.getHours()).slice(-2) + ':' +
|
||||
('0' + u.getMinutes()).slice(-2) + ':' +
|
||||
('0' + u.getSeconds()).slice(-2) + '.' +
|
||||
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
|
||||
},
|
||||
clearConsole() {
|
||||
this.consoleBuffer = "";
|
||||
},
|
||||
copyConsole() {
|
||||
var input = document.createElement('textarea');
|
||||
input.innerHTML = this.consoleBuffer;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#console {
|
||||
background-color: #0C0C0C;
|
||||
color: #CCCCCC;
|
||||
padding: 8px;
|
||||
font-family: courier new;
|
||||
font-size: .875em;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<BasePage :title="$t('console.Console')" :isLoading="dataLoading">
|
||||
<CardElement :text="$t('console.VirtualDebugConsole')" textVariant="text-bg-primary">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoScroll"
|
||||
v-model="isAutoScroll">
|
||||
<label class="form-check-label" for="autoScroll">
|
||||
{{ $t('console.EnableAutoScroll') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" :onClick="clearConsole">
|
||||
{{ $t('console.ClearConsole') }}</button>
|
||||
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
|
||||
{{ $t('console.CopyToClipboard') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
|
||||
</CardElement>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import { authUrl } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
CardElement,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataLoading: true,
|
||||
consoleBuffer: "",
|
||||
isAutoScroll: true,
|
||||
endWithNewline: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initSocket();
|
||||
this.dataLoading = false;
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
watch: {
|
||||
consoleBuffer() {
|
||||
if (this.isAutoScroll) {
|
||||
const textarea = this.$el.querySelector("#console");
|
||||
setTimeout(() => {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initSocket() {
|
||||
console.log("Starting connection to WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/console`;
|
||||
|
||||
this.closeSocket();
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
|
||||
let outstr = new String(event.data);
|
||||
let removedNewline = false;
|
||||
if (outstr.endsWith('\n')) {
|
||||
outstr = outstr.substring(0, outstr.length - 1);
|
||||
removedNewline = true;
|
||||
}
|
||||
this.consoleBuffer += (this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll("\n", "\n" + this.getOutDate());
|
||||
this.endWithNewline = removedNewline;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 5 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch {
|
||||
// continue regardless of error
|
||||
}
|
||||
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
},
|
||||
getOutDate(): string {
|
||||
const u = new Date();
|
||||
return ('0' + u.getHours()).slice(-2) + ':' +
|
||||
('0' + u.getMinutes()).slice(-2) + ':' +
|
||||
('0' + u.getSeconds()).slice(-2) + '.' +
|
||||
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' > ';
|
||||
},
|
||||
clearConsole() {
|
||||
this.consoleBuffer = "";
|
||||
},
|
||||
copyConsole() {
|
||||
const input = document.createElement('textarea');
|
||||
input.innerHTML = this.consoleBuffer;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#console {
|
||||
background-color: #0C0C0C;
|
||||
color: #CCCCCC;
|
||||
padding: 8px;
|
||||
font-family: courier new;
|
||||
font-size: .875em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -219,7 +219,7 @@ export default defineComponent({
|
||||
getPinMappingList() {
|
||||
this.pinMappingLoading = true;
|
||||
fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router, true))
|
||||
.then(
|
||||
(data) => {
|
||||
this.pinMappingList = data;
|
||||
@ -246,6 +246,9 @@ export default defineComponent({
|
||||
.then(
|
||||
(data) => {
|
||||
this.deviceConfigList = data;
|
||||
if (this.deviceConfigList.curPin.name === "") {
|
||||
this.deviceConfigList.curPin.name = "Default";
|
||||
}
|
||||
this.dataLoading = false;
|
||||
}
|
||||
)
|
||||
|
||||
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";
|
||||
|
||||
// Use a simple fetch request to check if the remote host is reachable
|
||||
fetch(remoteHostUrl, { method: 'HEAD' })
|
||||
fetch(remoteHostUrl, { method: 'GET' })
|
||||
.then(response => {
|
||||
// Check if the response status is OK (200-299 range)
|
||||
if (response.ok) {
|
||||
|
||||
@ -5,14 +5,20 @@
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]">
|
||||
<div class="nav nav-pills row-cols-sm-1" id="v-pills-tab" role="tablist" aria-orientation="vertical">
|
||||
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link"
|
||||
<button v-for="inverter in inverterData" :key="inverter.serial" class="nav-link border border-primary text-break"
|
||||
:id="'v-pills-' + inverter.serial + '-tab'" data-bs-toggle="pill"
|
||||
:data-bs-target="'#v-pills-' + inverter.serial" type="button" role="tab"
|
||||
aria-controls="'v-pills-' + inverter.serial" aria-selected="true">
|
||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||
{{ inverter.name }}
|
||||
<div class="row">
|
||||
<div class="col-auto col-sm-2">
|
||||
<BIconXCircleFill class="fs-4" v-if="!inverter.reachable" />
|
||||
<BIconExclamationCircleFill class="fs-4" v-if="inverter.reachable && !inverter.producing" />
|
||||
<BIconCheckCircleFill class="fs-4" v-if="inverter.reachable && inverter.producing" />
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
{{ inverter.name }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -346,7 +352,7 @@ export default defineComponent({
|
||||
showAlertLimit: false,
|
||||
|
||||
powerSettingView: {} as bootstrap.Modal,
|
||||
powerSettingSerial: 0,
|
||||
powerSettingSerial: "",
|
||||
powerSettingLoading: true,
|
||||
alertMessagePower: "",
|
||||
alertTypePower: "info",
|
||||
@ -470,17 +476,15 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
var self = this;
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
this.socket.onopen = (event) => {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
self.isWebsocketConnected = true;
|
||||
this.isWebsocketConnected = true;
|
||||
};
|
||||
|
||||
this.socket.onclose = function () {
|
||||
this.socket.onclose = () => {
|
||||
console.log("Connection to websocket closed...")
|
||||
self.isWebsocketConnected = false;
|
||||
this.isWebsocketConnected = false;
|
||||
}
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
@ -515,7 +519,7 @@ export default defineComponent({
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
onShowEventlog(serial: number) {
|
||||
onShowEventlog(serial: string) {
|
||||
this.eventLogLoading = true;
|
||||
fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
@ -526,7 +530,7 @@ export default defineComponent({
|
||||
|
||||
this.eventLogView.show();
|
||||
},
|
||||
onShowDevInfo(serial: number) {
|
||||
onShowDevInfo(serial: string) {
|
||||
this.devInfoLoading = true;
|
||||
fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
@ -538,7 +542,7 @@ export default defineComponent({
|
||||
|
||||
this.devInfoView.show();
|
||||
},
|
||||
onShowGridProfile(serial: number) {
|
||||
onShowGridProfile(serial: string) {
|
||||
this.gridProfileLoading = true;
|
||||
fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
@ -555,9 +559,9 @@ export default defineComponent({
|
||||
|
||||
this.gridProfileView.show();
|
||||
},
|
||||
onShowLimitSettings(serial: number) {
|
||||
onShowLimitSettings(serial: string) {
|
||||
this.showAlertLimit = false;
|
||||
this.targetLimitList.serial = 0;
|
||||
this.targetLimitList.serial = "";
|
||||
this.targetLimitList.limit_value = 0;
|
||||
this.targetLimitType = 1;
|
||||
this.targetLimitTypeText = this.$t('home.Relative');
|
||||
@ -611,9 +615,9 @@ export default defineComponent({
|
||||
this.targetLimitType = type;
|
||||
},
|
||||
|
||||
onShowPowerSettings(serial: number) {
|
||||
onShowPowerSettings(serial: string) {
|
||||
this.showAlertPower = false;
|
||||
this.powerSettingSerial = 0;
|
||||
this.powerSettingSerial = "";
|
||||
this.powerSettingLoading = true;
|
||||
fetch("/api/power/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
|
||||
@ -8,8 +8,7 @@
|
||||
<form class="form-inline" v-on:submit.prevent="onSubmit">
|
||||
<div class="form-group">
|
||||
<label>{{ $t('inverteradmin.Serial') }}</label>
|
||||
<input v-model="newInverterData.serial" type="number" class="form-control ml-sm-2 mr-sm-4 my-2"
|
||||
required />
|
||||
<InputSerial v-model="newInverterData.serial" inputClass="ml-sm-2 mr-sm-4 my-2" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ $t('inverteradmin.Name') }}</label>
|
||||
@ -91,7 +90,7 @@
|
||||
<label for="inverter-serial" class="col-form-label">
|
||||
{{ $t('inverteradmin.InverterSerial') }}
|
||||
</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') }}
|
||||
<BIconInfoCircle v-tooltip :title="$t('inverteradmin.InverterNameHint')" />
|
||||
</label>
|
||||
@ -207,6 +206,7 @@ import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import InputSerial from '@/components/InputSerial.vue';
|
||||
import ModalDialog from '@/components/ModalDialog.vue';
|
||||
import type { Inverter } from '@/types/InverterConfig';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
@ -235,6 +235,7 @@ export default defineComponent({
|
||||
BootstrapAlert,
|
||||
CardElement,
|
||||
InputElement,
|
||||
InputSerial,
|
||||
ModalDialog,
|
||||
BIconInfoCircle,
|
||||
BIconPencil,
|
||||
|
||||
@ -58,12 +58,16 @@ export default defineComponent({
|
||||
})
|
||||
},
|
||||
getUpdateInfo() {
|
||||
if (this.systemDataList.git_hash === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash;
|
||||
|
||||
// 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_is_hash = true;
|
||||
}
|
||||
@ -95,9 +99,9 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
allowVersionInfo(allow: Boolean) {
|
||||
allowVersionInfo(allow: boolean) {
|
||||
localStorage.setItem("allowVersionInfo", allow ? "1" : "0");
|
||||
if (allow) {
|
||||
localStorage.setItem("allowVersionInfo", this.allowVersionInfo ? "1" : "0");
|
||||
this.getUpdateInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import path from 'path'
|
||||
// example 'vite.user.ts': export const proxy_target = '192.168.16.107'
|
||||
let proxy_target;
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
proxy_target = require('./vite.user.ts').proxy_target;
|
||||
} catch (error) {
|
||||
proxy_target = '192.168.20.110';
|
||||
@ -29,6 +30,7 @@ export default defineConfig({
|
||||
fullInstall: false,
|
||||
forceStringify: true,
|
||||
strictMessage: false,
|
||||
jitCompilation: false,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
|
||||
1275
webapp/yarn.lock
1275
webapp/yarn.lock
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user