Merge remote-tracking branch 'tbnobody/OpenDTU/master'
@ -195,7 +195,8 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw
|
|||||||
A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md)
|
A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md)
|
||||||
|
|
||||||
## Available cases
|
## Available cases
|
||||||
* [https://www.thingiverse.com/thing:5435911](https://www.thingiverse.com/thing:5435911)
|
* <https://www.thingiverse.com/thing:5435911>
|
||||||
|
* <https://www.printables.com/model/293003-sol-opendtu-esp32-nrf24l01-case>
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
* Building the WebApp
|
* Building the WebApp
|
||||||
|
|||||||
@ -69,3 +69,4 @@ cmd topics are used to set values. Status topics are updated from values set in
|
|||||||
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
|
| [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % |
|
||||||
| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
|
| [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) |
|
||||||
| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 |
|
| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 |
|
||||||
|
| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 |
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
BIN
docs/screenshots/15_LimitSettings.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/screenshots/16_PowerSettings.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/screenshots/17_InverterInfo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@ -4,7 +4,8 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#define CONFIG_FILENAME "/config.bin"
|
#define CONFIG_FILENAME "/config.bin"
|
||||||
#define CONFIG_VERSION 0x00011500 // 0.1.21 // make sure to clean all after change
|
#define CONFIG_FILENAME_JSON "/config.json"
|
||||||
|
#define CONFIG_VERSION 0x00011600 // 0.1.22 // make sure to clean all after change
|
||||||
|
|
||||||
#define WIFI_MAX_SSID_STRLEN 31
|
#define WIFI_MAX_SSID_STRLEN 31
|
||||||
#define WIFI_MAX_PASSWORD_STRLEN 64
|
#define WIFI_MAX_PASSWORD_STRLEN 64
|
||||||
@ -26,6 +27,8 @@
|
|||||||
#define INV_MAX_COUNT 10
|
#define INV_MAX_COUNT 10
|
||||||
#define INV_MAX_CHAN_COUNT 4
|
#define INV_MAX_CHAN_COUNT 4
|
||||||
|
|
||||||
|
#define JSON_BUFFER_SIZE 6144
|
||||||
|
|
||||||
struct INVERTER_CONFIG_T {
|
struct INVERTER_CONFIG_T {
|
||||||
uint64_t Serial;
|
uint64_t Serial;
|
||||||
char Name[INV_MAX_NAME_STRLEN + 1];
|
char Name[INV_MAX_NAME_STRLEN + 1];
|
||||||
@ -82,6 +85,8 @@ struct CONFIG_T {
|
|||||||
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
||||||
|
|
||||||
bool Mqtt_Hass_Expire;
|
bool Mqtt_Hass_Expire;
|
||||||
|
|
||||||
|
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
class ConfigurationClass {
|
class ConfigurationClass {
|
||||||
@ -93,6 +98,9 @@ public:
|
|||||||
CONFIG_T& get();
|
CONFIG_T& get();
|
||||||
|
|
||||||
INVERTER_CONFIG_T* getFreeInverterSlot();
|
INVERTER_CONFIG_T* getFreeInverterSlot();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool readJson();
|
||||||
};
|
};
|
||||||
|
|
||||||
extern ConfigurationClass Configuration;
|
extern ConfigurationClass Configuration;
|
||||||
@ -12,6 +12,7 @@
|
|||||||
#include "WebApi_network.h"
|
#include "WebApi_network.h"
|
||||||
#include "WebApi_ntp.h"
|
#include "WebApi_ntp.h"
|
||||||
#include "WebApi_power.h"
|
#include "WebApi_power.h"
|
||||||
|
#include "WebApi_security.h"
|
||||||
#include "WebApi_sysstatus.h"
|
#include "WebApi_sysstatus.h"
|
||||||
#include "WebApi_webapp.h"
|
#include "WebApi_webapp.h"
|
||||||
#include "WebApi_ws_live.h"
|
#include "WebApi_ws_live.h"
|
||||||
@ -40,6 +41,7 @@ private:
|
|||||||
WebApiNetworkClass _webApiNetwork;
|
WebApiNetworkClass _webApiNetwork;
|
||||||
WebApiNtpClass _webApiNtp;
|
WebApiNtpClass _webApiNtp;
|
||||||
WebApiPowerClass _webApiPower;
|
WebApiPowerClass _webApiPower;
|
||||||
|
WebApiSecurityClass _webApiSecurity;
|
||||||
WebApiSysstatusClass _webApiSysstatus;
|
WebApiSysstatusClass _webApiSysstatus;
|
||||||
WebApiWebappClass _webApiWebapp;
|
WebApiWebappClass _webApiWebapp;
|
||||||
WebApiWsLiveClass _webApiWsLive;
|
WebApiWsLiveClass _webApiWsLive;
|
||||||
|
|||||||
16
include/WebApi_security.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
class WebApiSecurityClass {
|
||||||
|
public:
|
||||||
|
void init(AsyncWebServer* server);
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onPasswordGet(AsyncWebServerRequest* request);
|
||||||
|
void onPasswordPost(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
AsyncWebServer* _server;
|
||||||
|
};
|
||||||
@ -169,6 +169,11 @@ bool HoymilesRadio::isConnected()
|
|||||||
return _radio->isChipConnected();
|
return _radio->isChipConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HoymilesRadio::isPVariant()
|
||||||
|
{
|
||||||
|
return _radio->isPVariant();
|
||||||
|
}
|
||||||
|
|
||||||
void HoymilesRadio::openReadingPipe()
|
void HoymilesRadio::openReadingPipe()
|
||||||
{
|
{
|
||||||
serial_u s;
|
serial_u s;
|
||||||
|
|||||||
@ -47,6 +47,7 @@ public:
|
|||||||
|
|
||||||
bool isIdle();
|
bool isIdle();
|
||||||
bool isConnected();
|
bool isConnected();
|
||||||
|
bool isPVariant();
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
T* enqueCommand()
|
T* enqueCommand()
|
||||||
|
|||||||
@ -97,6 +97,10 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
|
|||||||
|
|
||||||
bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type)
|
bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type)
|
||||||
{
|
{
|
||||||
|
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
|
||||||
|
limit = min<float>(100, limit);
|
||||||
|
}
|
||||||
|
|
||||||
_activePowerControlLimit = limit;
|
_activePowerControlLimit = limit;
|
||||||
_activePowerControlType = type;
|
_activePowerControlType = type;
|
||||||
|
|
||||||
@ -115,7 +119,11 @@ bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio)
|
|||||||
|
|
||||||
bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn)
|
bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn)
|
||||||
{
|
{
|
||||||
_powerState = turnOn;
|
if (turnOn) {
|
||||||
|
_powerState = 1;
|
||||||
|
} else {
|
||||||
|
_powerState = 0;
|
||||||
|
}
|
||||||
|
|
||||||
PowerControlCommand* cmd = radio->enqueCommand<PowerControlCommand>();
|
PowerControlCommand* cmd = radio->enqueCommand<PowerControlCommand>();
|
||||||
cmd->setPowerOn(turnOn);
|
cmd->setPowerOn(turnOn);
|
||||||
@ -125,7 +133,33 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio)
|
||||||
|
{
|
||||||
|
_powerState = 2;
|
||||||
|
|
||||||
|
PowerControlCommand* cmd = radio->enqueCommand<PowerControlCommand>();
|
||||||
|
cmd->setRestart();
|
||||||
|
cmd->setTargetAddress(serial());
|
||||||
|
PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool HM_Abstract::resendPowerControlRequest(HoymilesRadio* radio)
|
bool HM_Abstract::resendPowerControlRequest(HoymilesRadio* radio)
|
||||||
{
|
{
|
||||||
return sendPowerControlRequest(radio, _powerState);
|
switch (_powerState) {
|
||||||
|
case 0:
|
||||||
|
return sendPowerControlRequest(radio, false);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
return sendPowerControlRequest(radio, true);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
return sendRestartControlRequest(radio);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -12,6 +12,7 @@ public:
|
|||||||
bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type);
|
bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type);
|
||||||
bool resendActivePowerControlRequest(HoymilesRadio* radio);
|
bool resendActivePowerControlRequest(HoymilesRadio* radio);
|
||||||
bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn);
|
bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn);
|
||||||
|
bool sendRestartControlRequest(HoymilesRadio* radio);
|
||||||
bool resendPowerControlRequest(HoymilesRadio* radio);
|
bool resendPowerControlRequest(HoymilesRadio* radio);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -19,5 +20,5 @@ private:
|
|||||||
float _activePowerControlLimit = 0;
|
float _activePowerControlLimit = 0;
|
||||||
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;
|
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;
|
||||||
|
|
||||||
bool _powerState = true;
|
uint8_t _powerState = 1;
|
||||||
};
|
};
|
||||||
@ -53,6 +53,7 @@ public:
|
|||||||
virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0;
|
virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0;
|
||||||
virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0;
|
virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0;
|
||||||
virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0;
|
virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0;
|
||||||
|
virtual bool sendRestartControlRequest(HoymilesRadio* radio) = 0;
|
||||||
virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0;
|
virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0;
|
||||||
|
|
||||||
AlarmLogParser* EventLog();
|
AlarmLogParser* EventLog();
|
||||||
|
|||||||
@ -19,7 +19,7 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui
|
|||||||
|
|
||||||
float SystemConfigParaParser::getLimitPercent()
|
float SystemConfigParaParser::getLimitPercent()
|
||||||
{
|
{
|
||||||
return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10;
|
return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SystemConfigParaParser::setLimitPercent(float value)
|
void SystemConfigParaParser::setLimitPercent(float value)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ extra_scripts =
|
|||||||
|
|
||||||
board_build.partitions = partitions_custom.csv
|
board_build.partitions = partitions_custom.csv
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
monitor_filters = time, colorize, log2file, esp32_exception_decoder
|
monitor_filters = esp32_exception_decoder, time, log2file, colorize
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
upload_protocol = esptool
|
upload_protocol = esptool
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
#include "Configuration.h"
|
#include "Configuration.h"
|
||||||
#include "defaults.h"
|
#include "defaults.h"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
|
||||||
CONFIG_T config;
|
CONFIG_T config;
|
||||||
@ -58,6 +59,8 @@ void ConfigurationClass::init()
|
|||||||
strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
||||||
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
|
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
|
||||||
|
|
||||||
|
strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password));
|
||||||
|
|
||||||
config.Vedirect_Enabled = VEDIRECT_ENABLED;
|
config.Vedirect_Enabled = VEDIRECT_ENABLED;
|
||||||
config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY;
|
config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY;
|
||||||
config.Vedirect_PollInterval = VEDIRECT_POLL_INTERVAL;
|
config.Vedirect_PollInterval = VEDIRECT_POLL_INTERVAL;
|
||||||
@ -65,21 +68,94 @@ void ConfigurationClass::init()
|
|||||||
|
|
||||||
bool ConfigurationClass::write()
|
bool ConfigurationClass::write()
|
||||||
{
|
{
|
||||||
File f = LittleFS.open(CONFIG_FILENAME, "w");
|
File f = LittleFS.open(CONFIG_FILENAME_JSON, "w");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
config.Cfg_SaveCount++;
|
config.Cfg_SaveCount++;
|
||||||
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
|
|
||||||
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
|
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||||
f.write(bytes[i]);
|
|
||||||
|
JsonObject cfg = doc.createNestedObject("cfg");
|
||||||
|
cfg["version"] = config.Cfg_Version;
|
||||||
|
cfg["save_count"] = config.Cfg_SaveCount;
|
||||||
|
|
||||||
|
JsonObject wifi = doc.createNestedObject("wifi");
|
||||||
|
wifi["ssid"] = config.WiFi_Ssid;
|
||||||
|
wifi["password"] = config.WiFi_Password;
|
||||||
|
wifi["ip"] = IPAddress(config.WiFi_Ip).toString();
|
||||||
|
wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString();
|
||||||
|
wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString();
|
||||||
|
wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString();
|
||||||
|
wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString();
|
||||||
|
wifi["dhcp"] = config.WiFi_Dhcp;
|
||||||
|
wifi["hostname"] = config.WiFi_Hostname;
|
||||||
|
|
||||||
|
JsonObject ntp = doc.createNestedObject("ntp");
|
||||||
|
ntp["server"] = config.Ntp_Server;
|
||||||
|
ntp["timezone"] = config.Ntp_Timezone;
|
||||||
|
ntp["timezone_descr"] = config.Ntp_TimezoneDescr;
|
||||||
|
|
||||||
|
JsonObject mqtt = doc.createNestedObject("mqtt");
|
||||||
|
mqtt["enabled"] = config.Mqtt_Enabled;
|
||||||
|
mqtt["hostname"] = config.Mqtt_Hostname;
|
||||||
|
mqtt["port"] = config.Mqtt_Port;
|
||||||
|
mqtt["username"] = config.Mqtt_Username;
|
||||||
|
mqtt["password"] = config.Mqtt_Password;
|
||||||
|
mqtt["topic"] = config.Mqtt_Topic;
|
||||||
|
mqtt["retain"] = config.Mqtt_Retain;
|
||||||
|
mqtt["publish_invterval"] = config.Mqtt_PublishInterval;
|
||||||
|
|
||||||
|
JsonObject mqtt_lwt = mqtt.createNestedObject("lwt");
|
||||||
|
mqtt_lwt["topic"] = config.Mqtt_LwtTopic;
|
||||||
|
mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online;
|
||||||
|
mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline;
|
||||||
|
|
||||||
|
JsonObject mqtt_tls = mqtt.createNestedObject("tls");
|
||||||
|
mqtt_tls["enabled"] = config.Mqtt_Tls;
|
||||||
|
mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert;
|
||||||
|
|
||||||
|
JsonObject mqtt_hass = mqtt.createNestedObject("hass");
|
||||||
|
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");
|
||||||
|
dtu["serial"] = config.Dtu_Serial;
|
||||||
|
dtu["poll_interval"] = config.Dtu_PollInterval;
|
||||||
|
dtu["pa_level"] = config.Dtu_PaLevel;
|
||||||
|
|
||||||
|
JsonObject security = doc.createNestedObject("security");
|
||||||
|
security["password"] = config.Security_Password;
|
||||||
|
|
||||||
|
JsonArray inverters = doc.createNestedArray("inverters");
|
||||||
|
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||||
|
JsonObject inv = inverters.createNestedObject();
|
||||||
|
inv["serial"] = config.Inverter[i].Serial;
|
||||||
|
inv["name"] = config.Inverter[i].Name;
|
||||||
|
|
||||||
|
JsonArray channels = inv.createNestedArray("channels");
|
||||||
|
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||||
|
channels.add(config.Inverter[i].MaxChannelPower[c]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize JSON to file
|
||||||
|
if (serializeJson(doc, f) == 0) {
|
||||||
|
Serial.println("Failed to write file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigurationClass::read()
|
bool ConfigurationClass::read()
|
||||||
{
|
{
|
||||||
|
if (!LittleFS.exists(CONFIG_FILENAME_JSON)) {
|
||||||
|
Serial.println("Converting binary config to json... ");
|
||||||
File f = LittleFS.open(CONFIG_FILENAME, "r");
|
File f = LittleFS.open(CONFIG_FILENAME, "r");
|
||||||
if (!f) {
|
if (!f) {
|
||||||
return false;
|
return false;
|
||||||
@ -88,6 +164,125 @@ bool ConfigurationClass::read()
|
|||||||
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
|
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
|
||||||
bytes[i] = f.read();
|
bytes[i] = f.read();
|
||||||
}
|
}
|
||||||
|
f.close();
|
||||||
|
write();
|
||||||
|
Serial.println("done");
|
||||||
|
LittleFS.remove(CONFIG_FILENAME);
|
||||||
|
}
|
||||||
|
return readJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigurationClass::readJson()
|
||||||
|
{
|
||||||
|
File f = LittleFS.open(CONFIG_FILENAME_JSON, "r", false);
|
||||||
|
if (!f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||||
|
// Deserialize the JSON document
|
||||||
|
DeserializationError error = deserializeJson(doc, f);
|
||||||
|
if (error) {
|
||||||
|
Serial.println(F("Failed to read file, using default configuration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject cfg = doc["cfg"];
|
||||||
|
config.Cfg_Version = cfg["version"] | CONFIG_VERSION;
|
||||||
|
config.Cfg_SaveCount = cfg["save_count"] | 0;
|
||||||
|
|
||||||
|
JsonObject wifi = doc["wifi"];
|
||||||
|
strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid));
|
||||||
|
strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password));
|
||||||
|
strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname));
|
||||||
|
|
||||||
|
IPAddress wifi_ip;
|
||||||
|
wifi_ip.fromString(wifi["ip"] | "");
|
||||||
|
config.WiFi_Ip[0] = wifi_ip[0];
|
||||||
|
config.WiFi_Ip[1] = wifi_ip[1];
|
||||||
|
config.WiFi_Ip[2] = wifi_ip[2];
|
||||||
|
config.WiFi_Ip[3] = wifi_ip[3];
|
||||||
|
|
||||||
|
IPAddress wifi_netmask;
|
||||||
|
wifi_netmask.fromString(wifi["netmask"] | "");
|
||||||
|
config.WiFi_Netmask[0] = wifi_netmask[0];
|
||||||
|
config.WiFi_Netmask[1] = wifi_netmask[1];
|
||||||
|
config.WiFi_Netmask[2] = wifi_netmask[2];
|
||||||
|
config.WiFi_Netmask[3] = wifi_netmask[3];
|
||||||
|
|
||||||
|
IPAddress wifi_gateway;
|
||||||
|
wifi_gateway.fromString(wifi["gateway"] | "");
|
||||||
|
config.WiFi_Gateway[0] = wifi_gateway[0];
|
||||||
|
config.WiFi_Gateway[1] = wifi_gateway[1];
|
||||||
|
config.WiFi_Gateway[2] = wifi_gateway[2];
|
||||||
|
config.WiFi_Gateway[3] = wifi_gateway[3];
|
||||||
|
|
||||||
|
IPAddress wifi_dns1;
|
||||||
|
wifi_dns1.fromString(wifi["dns1"] | "");
|
||||||
|
config.WiFi_Dns1[0] = wifi_dns1[0];
|
||||||
|
config.WiFi_Dns1[1] = wifi_dns1[1];
|
||||||
|
config.WiFi_Dns1[2] = wifi_dns1[2];
|
||||||
|
config.WiFi_Dns1[3] = wifi_dns1[3];
|
||||||
|
|
||||||
|
IPAddress wifi_dns2;
|
||||||
|
wifi_dns2.fromString(wifi["dns2"] | "");
|
||||||
|
config.WiFi_Dns2[0] = wifi_dns2[0];
|
||||||
|
config.WiFi_Dns2[1] = wifi_dns2[1];
|
||||||
|
config.WiFi_Dns2[2] = wifi_dns2[2];
|
||||||
|
config.WiFi_Dns2[3] = wifi_dns2[3];
|
||||||
|
|
||||||
|
config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP;
|
||||||
|
|
||||||
|
JsonObject ntp = doc["ntp"];
|
||||||
|
strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server));
|
||||||
|
strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone));
|
||||||
|
strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr));
|
||||||
|
|
||||||
|
JsonObject mqtt = doc["mqtt"];
|
||||||
|
config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED;
|
||||||
|
strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname));
|
||||||
|
config.Mqtt_Port = mqtt["port"] | MQTT_PORT;
|
||||||
|
strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username));
|
||||||
|
strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password));
|
||||||
|
strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic));
|
||||||
|
config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN;
|
||||||
|
config.Mqtt_PublishInterval = mqtt["publish_invterval"] | MQTT_PUBLISH_INTERVAL;
|
||||||
|
|
||||||
|
JsonObject mqtt_lwt = mqtt["lwt"];
|
||||||
|
strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic));
|
||||||
|
strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online));
|
||||||
|
strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline));
|
||||||
|
|
||||||
|
JsonObject mqtt_tls = mqtt["tls"];
|
||||||
|
config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS;
|
||||||
|
strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
|
||||||
|
|
||||||
|
JsonObject mqtt_hass = mqtt["hass"];
|
||||||
|
config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED;
|
||||||
|
config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN;
|
||||||
|
config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE;
|
||||||
|
config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS;
|
||||||
|
strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
||||||
|
|
||||||
|
JsonObject dtu = doc["dtu"];
|
||||||
|
config.Dtu_Serial = dtu["serial"] | DTU_SERIAL;
|
||||||
|
config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL;
|
||||||
|
config.Dtu_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL;
|
||||||
|
|
||||||
|
JsonObject security = doc["security"];
|
||||||
|
strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password));
|
||||||
|
|
||||||
|
JsonArray inverters = doc["inverters"];
|
||||||
|
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||||
|
JsonObject inv = inverters[i].as<JsonObject>();
|
||||||
|
config.Inverter[i].Serial = inv["serial"] | 0ULL;
|
||||||
|
strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name));
|
||||||
|
|
||||||
|
JsonArray channels = inv["channels"];
|
||||||
|
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||||
|
config.Inverter[i].MaxChannelPower[c] = channels[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f.close();
|
f.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -163,6 +358,10 @@ void ConfigurationClass::migrate()
|
|||||||
config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE;
|
config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.Cfg_Version < 0x00011600) {
|
||||||
|
strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password));
|
||||||
|
}
|
||||||
|
|
||||||
config.Cfg_Version = CONFIG_VERSION;
|
config.Cfg_Version = CONFIG_VERSION;
|
||||||
write();
|
write();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
|
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
|
||||||
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
|
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
|
||||||
#define TOPIC_SUB_POWER "power"
|
#define TOPIC_SUB_POWER "power"
|
||||||
|
#define TOPIC_SUB_RESTART "restart"
|
||||||
|
|
||||||
MqttSettingsClass::MqttSettingsClass()
|
MqttSettingsClass::MqttSettingsClass()
|
||||||
{
|
{
|
||||||
@ -48,6 +49,7 @@ void MqttSettingsClass::onMqttConnect(bool sessionPresent)
|
|||||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0);
|
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0);
|
||||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0);
|
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0);
|
||||||
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0);
|
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0);
|
||||||
|
mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason)
|
void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason)
|
||||||
@ -127,7 +129,6 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie
|
|||||||
|
|
||||||
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
|
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
|
||||||
// Set inverter limit relative persistent
|
// Set inverter limit relative persistent
|
||||||
payload_val = min<uint32_t>(100, payload_val);
|
|
||||||
Serial.printf("Limit Persistent: %d %%\n", payload_val);
|
Serial.printf("Limit Persistent: %d %%\n", payload_val);
|
||||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent);
|
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent);
|
||||||
|
|
||||||
@ -138,7 +139,6 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie
|
|||||||
|
|
||||||
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) {
|
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) {
|
||||||
// Set inverter limit relative non persistent
|
// Set inverter limit relative non persistent
|
||||||
payload_val = min<uint32_t>(100, payload_val);
|
|
||||||
Serial.printf("Limit Non-Persistent: %d %%\n", payload_val);
|
Serial.printf("Limit Non-Persistent: %d %%\n", payload_val);
|
||||||
if (!properties.retain) {
|
if (!properties.retain) {
|
||||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent);
|
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent);
|
||||||
@ -159,6 +159,15 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie
|
|||||||
// Turn inverter on or off
|
// Turn inverter on or off
|
||||||
Serial.printf("Set inverter power to: %d\n", payload_val);
|
Serial.printf("Set inverter power to: %d\n", payload_val);
|
||||||
inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0);
|
inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0);
|
||||||
|
|
||||||
|
} else if (!strcmp(setting, TOPIC_SUB_RESTART)) {
|
||||||
|
// Restart inverter
|
||||||
|
Serial.printf("Restart inverter\n");
|
||||||
|
if (!properties.retain && payload_val == 1) {
|
||||||
|
inv->sendRestartControlRequest(Hoymiles.getRadio());
|
||||||
|
} else {
|
||||||
|
Serial.println("Ignored because retained");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,7 @@ void NetworkSettingsClass::setupMode()
|
|||||||
WiFi.mode(WIFI_AP_STA);
|
WiFi.mode(WIFI_AP_STA);
|
||||||
String ssidString = getApName();
|
String ssidString = getApName();
|
||||||
WiFi.softAPConfig(apIp, apIp, apNetmask);
|
WiFi.softAPConfig(apIp, apIp, apNetmask);
|
||||||
WiFi.softAP((const char*)ssidString.c_str(), ACCESS_POINT_PASSWORD);
|
WiFi.softAP((const char*)ssidString.c_str(), Configuration.get().Security_Password);
|
||||||
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());
|
dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());
|
||||||
dnsServerStatus = true;
|
dnsServerStatus = true;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ void WebApiClass::init()
|
|||||||
_webApiNetwork.init(&_server);
|
_webApiNetwork.init(&_server);
|
||||||
_webApiNtp.init(&_server);
|
_webApiNtp.init(&_server);
|
||||||
_webApiPower.init(&_server);
|
_webApiPower.init(&_server);
|
||||||
|
_webApiSecurity.init(&_server);
|
||||||
_webApiSysstatus.init(&_server);
|
_webApiSysstatus.init(&_server);
|
||||||
_webApiWebapp.init(&_server);
|
_webApiWebapp.init(&_server);
|
||||||
_webApiWsLive.init(&_server);
|
_webApiWsLive.init(&_server);
|
||||||
@ -50,6 +51,7 @@ void WebApiClass::loop()
|
|||||||
_webApiNetwork.loop();
|
_webApiNetwork.loop();
|
||||||
_webApiNtp.loop();
|
_webApiNtp.loop();
|
||||||
_webApiPower.loop();
|
_webApiPower.loop();
|
||||||
|
_webApiSecurity.loop();
|
||||||
_webApiSysstatus.loop();
|
_webApiSysstatus.loop();
|
||||||
_webApiWebapp.loop();
|
_webApiWebapp.loop();
|
||||||
_webApiWsLive.loop();
|
_webApiWsLive.loop();
|
||||||
|
|||||||
@ -32,7 +32,7 @@ void WebApiConfigClass::loop()
|
|||||||
|
|
||||||
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
||||||
{
|
{
|
||||||
request->send(LittleFS, CONFIG_FILENAME, String(), true);
|
request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||||
@ -87,7 +87,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
|||||||
response->setLength();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
|
|
||||||
LittleFS.remove(CONFIG_FILENAME);
|
LittleFS.remove(CONFIG_FILENAME_JSON);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi
|
|||||||
{
|
{
|
||||||
if (!index) {
|
if (!index) {
|
||||||
// open the file on first call and store the file handle in the request object
|
// open the file on first call and store the file handle in the request object
|
||||||
request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w");
|
request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len) {
|
if (len) {
|
||||||
|
|||||||
@ -36,6 +36,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
|
|||||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||||
|
|
||||||
root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
|
root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
|
||||||
|
root[buffer]["max_power"] = inv->DevInfo()->getMaxPower();
|
||||||
|
|
||||||
LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess();
|
LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess();
|
||||||
String limitStatus = "Unknown";
|
String limitStatus = "Unknown";
|
||||||
|
|||||||
@ -170,6 +170,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!root[F("mqtt_topic")].as<String>().endsWith("/")) {
|
||||||
|
retMsg[F("message")] = F("Topic must end with slash (/)!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (root[F("mqtt_port")].as<uint>() == 0 || root[F("mqtt_port")].as<uint>() > 65535) {
|
if (root[F("mqtt_port")].as<uint>() == 0 || root[F("mqtt_port")].as<uint>() > 65535) {
|
||||||
retMsg[F("message")] = F("Port must be a number between 1 and 65535!");
|
retMsg[F("message")] = F("Port must be a number between 1 and 65535!");
|
||||||
response->setLength();
|
response->setLength();
|
||||||
|
|||||||
@ -151,12 +151,14 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
|
||||||
if (root[F("ssid")].as<String>().length() == 0 || root[F("ssid")].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
|
if (root[F("ssid")].as<String>().length() == 0 || root[F("ssid")].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
|
||||||
retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!");
|
retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!");
|
||||||
response->setLength();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (root[F("password")].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
|
if (root[F("password")].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
|
||||||
retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!");
|
retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!");
|
||||||
response->setLength();
|
response->setLength();
|
||||||
|
|||||||
@ -39,11 +39,9 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
|
|||||||
String limitStatus = "Unknown";
|
String limitStatus = "Unknown";
|
||||||
if (status == LastCommandSuccess::CMD_OK) {
|
if (status == LastCommandSuccess::CMD_OK) {
|
||||||
limitStatus = "Ok";
|
limitStatus = "Ok";
|
||||||
}
|
} else if (status == LastCommandSuccess::CMD_NOK) {
|
||||||
else if (status == LastCommandSuccess::CMD_NOK) {
|
|
||||||
limitStatus = "Failure";
|
limitStatus = "Failure";
|
||||||
}
|
} else if (status == LastCommandSuccess::CMD_PENDING) {
|
||||||
else if (status == LastCommandSuccess::CMD_PENDING) {
|
|
||||||
limitStatus = "Pending";
|
limitStatus = "Pending";
|
||||||
}
|
}
|
||||||
root[buffer]["power_set_status"] = limitStatus;
|
root[buffer]["power_set_status"] = limitStatus;
|
||||||
@ -86,7 +84,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(root.containsKey("serial")
|
if (!(root.containsKey("serial")
|
||||||
&& root.containsKey("power"))) {
|
&& (root.containsKey("power") || root.containsKey("restart")))) {
|
||||||
retMsg[F("message")] = F("Values are missing!");
|
retMsg[F("message")] = F("Values are missing!");
|
||||||
response->setLength();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
@ -101,8 +99,6 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint64_t serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
uint64_t serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||||
uint16_t power = root[F("power")].as<bool>();
|
|
||||||
|
|
||||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||||
if (inv == nullptr) {
|
if (inv == nullptr) {
|
||||||
retMsg[F("message")] = F("Invalid inverter specified!");
|
retMsg[F("message")] = F("Invalid inverter specified!");
|
||||||
@ -111,7 +107,14 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.containsKey("power")) {
|
||||||
|
uint16_t power = root[F("power")].as<bool>();
|
||||||
inv->sendPowerControlRequest(Hoymiles.getRadio(), power);
|
inv->sendPowerControlRequest(Hoymiles.getRadio(), power);
|
||||||
|
} else {
|
||||||
|
if (root[F("restart")].as<bool>()) {
|
||||||
|
inv->sendRestartControlRequest(Hoymiles.getRadio());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
retMsg[F("type")] = F("success");
|
retMsg[F("type")] = F("success");
|
||||||
retMsg[F("message")] = F("Settings saved!");
|
retMsg[F("message")] = F("Settings saved!");
|
||||||
|
|||||||
92
src/WebApi_security.cpp
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Thomas Basler and others
|
||||||
|
*/
|
||||||
|
#include "WebApi_security.h"
|
||||||
|
#include "ArduinoJson.h"
|
||||||
|
#include "AsyncJson.h"
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include "helper.h"
|
||||||
|
|
||||||
|
void WebApiSecurityClass::init(AsyncWebServer* server)
|
||||||
|
{
|
||||||
|
using std::placeholders::_1;
|
||||||
|
|
||||||
|
_server = server;
|
||||||
|
|
||||||
|
_server->on("/api/security/password", HTTP_GET, std::bind(&WebApiSecurityClass::onPasswordGet, this, _1));
|
||||||
|
_server->on("/api/security/password", HTTP_POST, std::bind(&WebApiSecurityClass::onPasswordPost, this, _1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiSecurityClass::loop()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||||
|
JsonObject root = response->getRoot();
|
||||||
|
const CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
root[F("password")] = config.Security_Password;
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request)
|
||||||
|
{
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||||
|
JsonObject retMsg = response->getRoot();
|
||||||
|
retMsg[F("type")] = F("warning");
|
||||||
|
|
||||||
|
if (!request->hasParam("data", true)) {
|
||||||
|
retMsg[F("message")] = F("No values found!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String json = request->getParam("data", true)->value();
|
||||||
|
|
||||||
|
if (json.length() > 1024) {
|
||||||
|
retMsg[F("message")] = F("Data too large!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicJsonDocument root(1024);
|
||||||
|
DeserializationError error = deserializeJson(root, json);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
retMsg[F("message")] = F("Failed to parse data!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!root.containsKey("password")) {
|
||||||
|
retMsg[F("message")] = F("Values are missing!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root[F("password")].as<String>().length() < 8 || root[F("password")].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN) {
|
||||||
|
retMsg[F("message")] = F("Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!");
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_T& config = Configuration.get();
|
||||||
|
strlcpy(config.Security_Password, root[F("password")].as<String>().c_str(), sizeof(config.Security_Password));
|
||||||
|
Configuration.write();
|
||||||
|
|
||||||
|
retMsg[F("type")] = F("success");
|
||||||
|
retMsg[F("message")] = F("Settings saved!");
|
||||||
|
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
}
|
||||||
@ -66,6 +66,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
|
|||||||
root[F("uptime")] = esp_timer_get_time() / 1000000;
|
root[F("uptime")] = esp_timer_get_time() / 1000000;
|
||||||
|
|
||||||
root[F("radio_connected")] = Hoymiles.getRadio()->isConnected();
|
root[F("radio_connected")] = Hoymiles.getRadio()->isConnected();
|
||||||
|
root[F("radio_pvariant")] = Hoymiles.getRadio()->isPVariant();
|
||||||
|
|
||||||
response->setLength();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
|
|||||||
@ -87,6 +87,12 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
|||||||
root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
|
root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
|
||||||
root[i][F("reachable")] = inv->isReachable();
|
root[i][F("reachable")] = inv->isReachable();
|
||||||
root[i][F("producing")] = inv->isProducing();
|
root[i][F("producing")] = inv->isProducing();
|
||||||
|
root[i][F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent();
|
||||||
|
if (inv->DevInfo()->getMaxPower() > 0) {
|
||||||
|
root[i][F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
|
||||||
|
} else {
|
||||||
|
root[i][F("limit_absolute")] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Loop all channels
|
// Loop all channels
|
||||||
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
|
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"@babel/core": "^7.19.3",
|
"@babel/core": "^7.19.3",
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@types/bootstrap": "^5.2.5",
|
"@types/bootstrap": "^5.2.5",
|
||||||
"@types/node": "^18.8.2",
|
"@types/node": "^18.8.3",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.2",
|
||||||
"@typescript-eslint/parser": "^5.38.1",
|
"@typescript-eslint/parser": "^5.38.1",
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.2",
|
"@vue/eslint-config-typescript": "^11.0.2",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.25.0",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"eslint-plugin-vue": "^9.6.0",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"vue-cli-plugin-compression": "~2.0.0"
|
"vue-cli-plugin-compression": "~2.0.0"
|
||||||
|
|||||||
@ -99,3 +99,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import {
|
||||||
|
BIconInfoCircle,
|
||||||
|
BIconActivity,
|
||||||
|
BIconBug,
|
||||||
|
BIconChat
|
||||||
|
} from 'bootstrap-icons-vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BIconInfoCircle,
|
||||||
|
BIconActivity,
|
||||||
|
BIconBug,
|
||||||
|
BIconChat,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@ -55,7 +55,7 @@
|
|||||||
|
|
||||||
<div v-else-if="!uploading">
|
<div v-else-if="!uploading">
|
||||||
<div class="form-group pt-2 mt-3">
|
<div class="form-group pt-2 mt-3">
|
||||||
<input class="form-control" type="file" ref="file" accept=".bin" @change="uploadConfig" />
|
<input class="form-control" type="file" ref="file" accept=".json" @change="uploadConfig" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -116,11 +116,19 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import {
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconArrowLeft,
|
||||||
|
BIconCheckCircle
|
||||||
|
} from 'bootstrap-icons-vue';
|
||||||
import * as bootstrap from 'bootstrap';
|
import * as bootstrap from 'bootstrap';
|
||||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconArrowLeft,
|
||||||
|
BIconCheckCircle,
|
||||||
BootstrapAlert,
|
BootstrapAlert,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -175,7 +183,7 @@ export default defineComponent({
|
|||||||
downloadConfig() {
|
downloadConfig() {
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = "/api/config/get"
|
link.href = "/api/config/get"
|
||||||
link.download = 'config.bin'
|
link.download = 'config.json'
|
||||||
link.click()
|
link.click()
|
||||||
},
|
},
|
||||||
uploadConfig(event: Event | null) {
|
uploadConfig(event: Event | null) {
|
||||||
|
|||||||
@ -74,8 +74,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import SparkMD5 from "spark-md5";
|
import SparkMD5 from "spark-md5";
|
||||||
|
import {
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconArrowLeft,
|
||||||
|
BIconArrowRepeat,
|
||||||
|
BIconCheckCircle
|
||||||
|
} from 'bootstrap-icons-vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconArrowLeft,
|
||||||
|
BIconArrowRepeat,
|
||||||
|
BIconCheckCircle,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
|
|||||||
@ -38,11 +38,26 @@
|
|||||||
'bg-warning': inverter.reachable && !inverter.producing,
|
'bg-warning': inverter.reachable && !inverter.producing,
|
||||||
'bg-primary': inverter.reachable && inverter.producing,
|
'bg-primary': inverter.reachable && inverter.producing,
|
||||||
}">
|
}">
|
||||||
{{ inverter.name }} (Inverter Serial Number:
|
<div class="p-2 flex-grow-1">
|
||||||
{{ inverter.serial }}) (Data Age:
|
<div class="d-flex flex-wrap">
|
||||||
{{ inverter.data_age }} seconds)
|
<div style="padding-right: 2em;">
|
||||||
|
{{ inverter.name }}
|
||||||
<div class="btn-toolbar" role="toolbar">
|
</div>
|
||||||
|
<div style="padding-right: 2em;">
|
||||||
|
Serial Number: {{ inverter.serial }}
|
||||||
|
</div>
|
||||||
|
<div style="padding-right: 2em;">
|
||||||
|
Current Limit: <template v-if="inverter.limit_absolute > -1"> {{
|
||||||
|
inverter.limit_absolute.toFixed(0) }}W | </template>{{
|
||||||
|
inverter.limit_relative.toFixed(0)
|
||||||
|
}}%
|
||||||
|
</div>
|
||||||
|
<div style="padding-right: 2em;">
|
||||||
|
Data Age: {{ inverter.data_age }} seconds
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-toolbar p-2" role="toolbar">
|
||||||
<div class="btn-group me-2" role="group">
|
<div class="btn-group me-2" role="group">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
@click="onShowLimitSettings(inverter.serial)"
|
@click="onShowLimitSettings(inverter.serial)"
|
||||||
@ -175,13 +190,22 @@
|
|||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
|
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
|
||||||
Limit:</label>
|
Limit:</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-4">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" class="form-control" id="inputCurrentLimit"
|
<input type="number" class="form-control" id="inputCurrentLimit"
|
||||||
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
|
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
|
||||||
<span class="input-group-text" id="currentLimitType">%</span>
|
<span class="input-group-text" id="currentLimitType">%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4" v-if="maxPower > 0">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" class="form-control" id="inputCurrentLimitAbsolute"
|
||||||
|
aria-describedby="currentLimitTypeAbsolute"
|
||||||
|
v-model="currentLimitAbsolute" disabled />
|
||||||
|
<span class="input-group-text" id="currentLimitTypeAbsolute">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3 align-items-center">
|
<div class="row mb-3 align-items-center">
|
||||||
@ -282,6 +306,9 @@
|
|||||||
<button type="button" class="btn btn-danger" @click="onSetPowerSettings(false)">
|
<button type="button" class="btn btn-danger" @click="onSetPowerSettings(false)">
|
||||||
<BIconToggleOff class="fs-4" /> Turn Off
|
<BIconToggleOff class="fs-4" /> Turn Off
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" @click="onSetPowerSettings(true, true)">
|
||||||
|
<BIconArrowCounterclockwise class="fs-4" /> Restart
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -298,11 +325,23 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
|
|
||||||
import * as bootstrap from 'bootstrap';
|
import * as bootstrap from 'bootstrap';
|
||||||
|
import {
|
||||||
|
BIconXCircleFill,
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconCheckCircleFill,
|
||||||
|
BIconSpeedometer,
|
||||||
|
BIconPower,
|
||||||
|
BIconCpu,
|
||||||
|
BIconJournalText,
|
||||||
|
BIconToggleOn,
|
||||||
|
BIconToggleOff,
|
||||||
|
BIconArrowCounterclockwise
|
||||||
|
} from 'bootstrap-icons-vue';
|
||||||
import EventLog from '@/components/partials/EventLog.vue';
|
import EventLog from '@/components/partials/EventLog.vue';
|
||||||
import DevInfo from '@/components/partials/DevInfo.vue';
|
import DevInfo from '@/components/partials/DevInfo.vue';
|
||||||
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
||||||
|
import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
|
||||||
import VedirectView from '@/components/partials/VedirectView.vue';
|
import VedirectView from '@/components/partials/VedirectView.vue';
|
||||||
|
|
||||||
declare interface Inverter {
|
declare interface Inverter {
|
||||||
@ -310,6 +349,8 @@ declare interface Inverter {
|
|||||||
name: string,
|
name: string,
|
||||||
reachable: boolean,
|
reachable: boolean,
|
||||||
producing: boolean,
|
producing: boolean,
|
||||||
|
limit_relative: 0,
|
||||||
|
limit_absolute: 0,
|
||||||
data_age: 0,
|
data_age: 0,
|
||||||
events: 0
|
events: 0
|
||||||
}
|
}
|
||||||
@ -320,6 +361,16 @@ export default defineComponent({
|
|||||||
EventLog,
|
EventLog,
|
||||||
DevInfo,
|
DevInfo,
|
||||||
BootstrapAlert,
|
BootstrapAlert,
|
||||||
|
BIconXCircleFill,
|
||||||
|
BIconExclamationCircleFill,
|
||||||
|
BIconCheckCircleFill,
|
||||||
|
BIconSpeedometer,
|
||||||
|
BIconPower,
|
||||||
|
BIconCpu,
|
||||||
|
BIconJournalText,
|
||||||
|
BIconToggleOn,
|
||||||
|
BIconToggleOff,
|
||||||
|
BIconArrowCounterclockwise,
|
||||||
VedirectView
|
VedirectView
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -342,7 +393,9 @@ export default defineComponent({
|
|||||||
limitSettingLoading: true,
|
limitSettingLoading: true,
|
||||||
|
|
||||||
currentLimit: 0,
|
currentLimit: 0,
|
||||||
|
currentLimitAbsolute: 0,
|
||||||
successCommandLimit: "",
|
successCommandLimit: "",
|
||||||
|
maxPower: 0,
|
||||||
targetLimit: 0,
|
targetLimit: 0,
|
||||||
targetLimitMin: 10,
|
targetLimitMin: 10,
|
||||||
targetLimitMax: 100,
|
targetLimitMax: 100,
|
||||||
@ -495,7 +548,11 @@ export default defineComponent({
|
|||||||
fetch("/api/limit/status")
|
fetch("/api/limit/status")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.currentLimit = data[serial].limit_relative;
|
this.maxPower = data[serial].max_power;
|
||||||
|
this.currentLimit = Number((data[serial].limit_relative).toFixed(1));
|
||||||
|
if (this.maxPower > 0) {
|
||||||
|
this.currentLimitAbsolute = Number((this.currentLimit * this.maxPower / 100).toFixed(1));
|
||||||
|
}
|
||||||
this.successCommandLimit = data[serial].limit_set_status;
|
this.successCommandLimit = data[serial].limit_set_status;
|
||||||
this.limitSettingSerial = serial;
|
this.limitSettingSerial = serial;
|
||||||
this.limitSettingLoading = false;
|
this.limitSettingLoading = false;
|
||||||
@ -572,11 +629,20 @@ export default defineComponent({
|
|||||||
this.showAlertPower = false;
|
this.showAlertPower = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
onSetPowerSettings(turnOn: boolean) {
|
onSetPowerSettings(turnOn: boolean, restart = false) {
|
||||||
const data = {
|
let data = {};
|
||||||
|
if (restart) {
|
||||||
|
data = {
|
||||||
|
serial: this.powerSettingSerial,
|
||||||
|
restart: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data = {
|
||||||
serial: this.powerSettingSerial,
|
serial: this.powerSettingSerial,
|
||||||
power: turnOn,
|
power: turnOn,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("data", JSON.stringify(data));
|
formData.append("data", JSON.stringify(data));
|
||||||
|
|
||||||
|
|||||||
@ -146,6 +146,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import {
|
||||||
|
BIconTrash,
|
||||||
|
BIconPencil
|
||||||
|
} from 'bootstrap-icons-vue';
|
||||||
import * as bootstrap from 'bootstrap';
|
import * as bootstrap from 'bootstrap';
|
||||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||||
|
|
||||||
@ -160,6 +164,8 @@ declare interface Inverter {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
BootstrapAlert,
|
BootstrapAlert,
|
||||||
|
BIconTrash,
|
||||||
|
BIconPencil,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -184,7 +190,7 @@ export default defineComponent({
|
|||||||
this.getInverters();
|
this.getInverters();
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedInverters() {
|
sortedInverters(): Inverter[] {
|
||||||
return this.inverters.slice().sort((a, b) => {
|
return this.inverters.slice().sort((a, b) => {
|
||||||
return a.serial - b.serial;
|
return a.serial - b.serial;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -30,6 +30,10 @@
|
|||||||
<router-link @click="onClick" class="dropdown-item" to="/settings/inverter">Inverter Settings
|
<router-link @click="onClick" class="dropdown-item" to="/settings/inverter">Inverter Settings
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link @click="onClick" class="dropdown-item" to="/settings/security">Security Settings
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">DTU Settings</router-link>
|
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">DTU Settings</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
123
webapp/src/components/SecurityAdminView.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-xxl" role="main">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Security Settings</h1>
|
||||||
|
</div>
|
||||||
|
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||||
|
{{ alertMessage }}
|
||||||
|
</BootstrapAlert>
|
||||||
|
|
||||||
|
<div class="text-center" v-if="dataLoading">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="!dataLoading">
|
||||||
|
<form @submit="savePasswordConfig">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-white bg-primary">Admin password</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="inputPassword" class="col-sm-2 col-form-label">Password:</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||||
|
placeholder="Password" v-model="password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label for="inputPasswordRepeat" class="col-sm-2 col-form-label">Repeat Password:</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="password" class="form-control" id="inputPasswordRepeat" maxlength="64"
|
||||||
|
placeholder="Password" v-model="passwordRepeat" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-secondary" role="alert">
|
||||||
|
<b>Hint:</b>
|
||||||
|
The administrator password is used to connect to the device when in AP mode.
|
||||||
|
It must be 8..64 characters.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary mb-3">Save</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BootstrapAlert,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dataLoading: true,
|
||||||
|
alertMessage: "",
|
||||||
|
alertType: "info",
|
||||||
|
showAlert: false,
|
||||||
|
|
||||||
|
password: "",
|
||||||
|
passwordRepeat: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getPasswordConfig();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getPasswordConfig() {
|
||||||
|
this.dataLoading = true;
|
||||||
|
fetch("/api/security/password")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then(
|
||||||
|
(data) => {
|
||||||
|
this.password = data["password"];
|
||||||
|
this.passwordRepeat = this.password;
|
||||||
|
this.dataLoading = false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
savePasswordConfig(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.password != this.passwordRepeat) {
|
||||||
|
this.alertMessage = "Passwords are not equal";
|
||||||
|
this.alertType = "warning";
|
||||||
|
this.showAlert = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
const data = {
|
||||||
|
password: this.password
|
||||||
|
}
|
||||||
|
formData.append("data", JSON.stringify(data));
|
||||||
|
|
||||||
|
fetch("/api/security/password", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw response.status;
|
||||||
|
} else {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(response) => {
|
||||||
|
this.alertMessage = response.message;
|
||||||
|
this.alertType = response.type;
|
||||||
|
this.showAlert = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -67,6 +67,7 @@ export default defineComponent({
|
|||||||
sketch_used: 0,
|
sketch_used: 0,
|
||||||
// RadioInfo
|
// RadioInfo
|
||||||
radio_connected: false,
|
radio_connected: false,
|
||||||
|
radio_pvariant: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
<td>Model</td>
|
<td>Model</td>
|
||||||
<td v-if="devInfoList.hw_model_name != ''">{{ devInfoList.hw_model_name }}</td>
|
<td v-if="devInfoList.hw_model_name != ''">{{ devInfoList.hw_model_name }}</td>
|
||||||
<td v-else>Unknown model! Please report the "Hardware Part Number" and model (e.g. HM-350) as an issue
|
<td v-else>Unknown model! Please report the "Hardware Part Number" and model (e.g. HM-350) as an issue
|
||||||
<a href="https://github.com/tbnobody/OpenDTU/issues" target="_blank">here</a>.</td>
|
<a href="https://github.com/tbnobody/OpenDTU/issues" target="_blank">here</a>.
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Bootloader Version</td>
|
<td>Bootloader Version</td>
|
||||||
@ -38,6 +39,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
import { BIconInfoSquare } from 'bootstrap-icons-vue';
|
||||||
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
|
||||||
|
|
||||||
declare interface DevInfoData {
|
declare interface DevInfoData {
|
||||||
@ -52,6 +54,7 @@ declare interface DevInfoData {
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
|
BIconInfoSquare,
|
||||||
BootstrapAlert,
|
BootstrapAlert,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -52,9 +52,6 @@ export default defineComponent({
|
|||||||
const dMins = minutes > 9 ? minutes : "0" + minutes;
|
const dMins = minutes > 9 ? minutes : "0" + minutes;
|
||||||
const dSecs = seconds > 9 ? seconds : "0" + seconds;
|
const dSecs = seconds > 9 ? seconds : "0" + seconds;
|
||||||
|
|
||||||
if (days > 0) {
|
|
||||||
return days + " " + dHours + ":" + dMins + ":" + dSecs;
|
|
||||||
}
|
|
||||||
return dHours + ":" + dMins + ":" + dSecs;
|
return dHours + ":" + dMins + ":" + dSecs;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,6 +17,18 @@
|
|||||||
<span v-else>not connected</span>
|
<span v-else>not connected</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Chip Type</th>
|
||||||
|
<td class="badge" :class="{
|
||||||
|
'bg-danger': radio_connected && !radio_pvariant,
|
||||||
|
'bg-success': radio_connected && radio_pvariant,
|
||||||
|
'bg-secondary': !radio_connected,
|
||||||
|
}">
|
||||||
|
<span v-if="radio_connected && radio_pvariant">nRF24L01+</span>
|
||||||
|
<span v-else-if="radio_connected && !radio_pvariant">nRF24L01</span>
|
||||||
|
<span v-else>Unknown</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -30,6 +42,7 @@ import { defineComponent } from 'vue';
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
radio_connected: { type: Boolean, required: true },
|
radio_connected: { type: Boolean, required: true },
|
||||||
|
radio_pvariant: { type: Boolean, required: true },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue';
|
|
||||||
|
|
||||||
import "bootstrap/dist/css/bootstrap.min.css"
|
import "bootstrap/dist/css/bootstrap.min.css"
|
||||||
import "bootstrap"
|
import "bootstrap"
|
||||||
|
|
||||||
createApp(App).use(router).use(BootstrapIconsPlugin).mount('#app')
|
createApp(App).use(router).mount('#app')
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
|
|||||||
import ConfigAdminView from '@/components/ConfigAdminView.vue'
|
import ConfigAdminView from '@/components/ConfigAdminView.vue'
|
||||||
import VedirectAdminView from '@/components/VedirectAdminView.vue'
|
import VedirectAdminView from '@/components/VedirectAdminView.vue'
|
||||||
import VedirectInfoView from '@/components/VedirectInfoView.vue'
|
import VedirectInfoView from '@/components/VedirectInfoView.vue'
|
||||||
|
import SecurityAdminView from '@/components/SecurityAdminView.vue'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
@ -90,6 +91,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/settings/config',
|
path: '/settings/config',
|
||||||
name: 'Config Management',
|
name: 'Config Management',
|
||||||
component: ConfigAdminView
|
component: ConfigAdminView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings/security',
|
||||||
|
name: 'Security',
|
||||||
|
component: SecurityAdminView
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -1164,10 +1164,10 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.19.1"
|
"@babel/helper-validator-identifier" "^7.19.1"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@eslint/eslintrc@^1.3.2":
|
"@eslint/eslintrc@^1.3.3":
|
||||||
version "1.3.2"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356"
|
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
|
||||||
integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==
|
integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "^6.12.4"
|
ajv "^6.12.4"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
@ -1200,11 +1200,6 @@
|
|||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
|
|
||||||
"@humanwhocodes/gitignore-to-minimatch@^1.0.2":
|
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
|
|
||||||
integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
|
|
||||||
|
|
||||||
"@humanwhocodes/module-importer@^1.0.1":
|
"@humanwhocodes/module-importer@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
|
||||||
@ -1469,10 +1464,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
|
||||||
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
|
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
|
||||||
|
|
||||||
"@types/node@^18.8.2":
|
"@types/node@^18.8.3":
|
||||||
version "18.8.2"
|
version "18.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
|
||||||
integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==
|
integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==
|
||||||
|
|
||||||
"@types/normalize-package-data@^2.4.0":
|
"@types/normalize-package-data@^2.4.0":
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
@ -3410,14 +3405,13 @@ eslint-webpack-plugin@^3.1.0:
|
|||||||
normalize-path "^3.0.0"
|
normalize-path "^3.0.0"
|
||||||
schema-utils "^3.1.1"
|
schema-utils "^3.1.1"
|
||||||
|
|
||||||
eslint@^8.24.0:
|
eslint@^8.25.0:
|
||||||
version "8.24.0"
|
version "8.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.25.0.tgz#00eb962f50962165d0c4ee3327708315eaa8058b"
|
||||||
integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==
|
integrity sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint/eslintrc" "^1.3.2"
|
"@eslint/eslintrc" "^1.3.3"
|
||||||
"@humanwhocodes/config-array" "^0.10.5"
|
"@humanwhocodes/config-array" "^0.10.5"
|
||||||
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
|
|
||||||
"@humanwhocodes/module-importer" "^1.0.1"
|
"@humanwhocodes/module-importer" "^1.0.1"
|
||||||
ajv "^6.10.0"
|
ajv "^6.10.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|||||||