Merge remote-tracking branch 'tbnobody/OpenDTU/master'

This commit is contained in:
helgeerbe 2022-10-17 10:10:34 +02:00
commit 5de35ee353
52 changed files with 726 additions and 82 deletions

View File

@ -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)
## 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 the WebApp

View File

@ -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_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/restart | W | Restarts the inverters (also resets YieldDay) | 1 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -4,7 +4,8 @@
#include <Arduino.h>
#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_PASSWORD_STRLEN 64
@ -26,6 +27,8 @@
#define INV_MAX_COUNT 10
#define INV_MAX_CHAN_COUNT 4
#define JSON_BUFFER_SIZE 6144
struct INVERTER_CONFIG_T {
uint64_t Serial;
char Name[INV_MAX_NAME_STRLEN + 1];
@ -82,6 +85,8 @@ struct CONFIG_T {
char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
bool Mqtt_Hass_Expire;
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
};
class ConfigurationClass {
@ -93,6 +98,9 @@ public:
CONFIG_T& get();
INVERTER_CONFIG_T* getFreeInverterSlot();
private:
bool readJson();
};
extern ConfigurationClass Configuration;

View File

@ -12,6 +12,7 @@
#include "WebApi_network.h"
#include "WebApi_ntp.h"
#include "WebApi_power.h"
#include "WebApi_security.h"
#include "WebApi_sysstatus.h"
#include "WebApi_webapp.h"
#include "WebApi_ws_live.h"
@ -40,6 +41,7 @@ private:
WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp;
WebApiPowerClass _webApiPower;
WebApiSecurityClass _webApiSecurity;
WebApiSysstatusClass _webApiSysstatus;
WebApiWebappClass _webApiWebapp;
WebApiWsLiveClass _webApiWsLive;

16
include/WebApi_security.h Normal file
View 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;
};

View File

@ -169,6 +169,11 @@ bool HoymilesRadio::isConnected()
return _radio->isChipConnected();
}
bool HoymilesRadio::isPVariant()
{
return _radio->isPVariant();
}
void HoymilesRadio::openReadingPipe()
{
serial_u s;

View File

@ -47,6 +47,7 @@ public:
bool isIdle();
bool isConnected();
bool isPVariant();
template <typename T>
T* enqueCommand()

View File

@ -97,6 +97,10 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type)
{
if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) {
limit = min<float>(100, limit);
}
_activePowerControlLimit = limit;
_activePowerControlType = type;
@ -115,7 +119,11 @@ bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio)
bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn)
{
_powerState = turnOn;
if (turnOn) {
_powerState = 1;
} else {
_powerState = 0;
}
PowerControlCommand* cmd = radio->enqueCommand<PowerControlCommand>();
cmd->setPowerOn(turnOn);
@ -125,7 +133,33 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn)
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)
{
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;
}
}

View File

@ -12,6 +12,7 @@ public:
bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type);
bool resendActivePowerControlRequest(HoymilesRadio* radio);
bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn);
bool sendRestartControlRequest(HoymilesRadio* radio);
bool resendPowerControlRequest(HoymilesRadio* radio);
private:
@ -19,5 +20,5 @@ private:
float _activePowerControlLimit = 0;
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;
bool _powerState = true;
uint8_t _powerState = 1;
};

View File

@ -53,6 +53,7 @@ public:
virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0;
virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0;
virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0;
virtual bool sendRestartControlRequest(HoymilesRadio* radio) = 0;
virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0;
AlarmLogParser* EventLog();

View File

@ -19,7 +19,7 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui
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)

View File

@ -31,7 +31,7 @@ extra_scripts =
board_build.partitions = partitions_custom.csv
board_build.filesystem = littlefs
monitor_filters = time, colorize, log2file, esp32_exception_decoder
monitor_filters = esp32_exception_decoder, time, log2file, colorize
monitor_speed = 115200
upload_protocol = esptool

View File

@ -4,6 +4,7 @@
*/
#include "Configuration.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
CONFIG_T config;
@ -58,6 +59,8 @@ void ConfigurationClass::init()
strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
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_UpdatesOnly = VEDIRECT_UPDATESONLY;
config.Vedirect_PollInterval = VEDIRECT_POLL_INTERVAL;
@ -65,29 +68,221 @@ void ConfigurationClass::init()
bool ConfigurationClass::write()
{
File f = LittleFS.open(CONFIG_FILENAME, "w");
File f = LittleFS.open(CONFIG_FILENAME_JSON, "w");
if (!f) {
return false;
}
config.Cfg_SaveCount++;
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
f.write(bytes[i]);
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
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();
return true;
}
bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r");
if (!LittleFS.exists(CONFIG_FILENAME_JSON)) {
Serial.println("Converting binary config to json... ");
File f = LittleFS.open(CONFIG_FILENAME, "r");
if (!f) {
return false;
}
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
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;
}
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
bytes[i] = f.read();
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();
return true;
}
@ -163,6 +358,10 @@ void ConfigurationClass::migrate()
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;
write();
}

View File

@ -15,6 +15,7 @@
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
#define TOPIC_SUB_POWER "power"
#define TOPIC_SUB_RESTART "restart"
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_ABSOLUTE).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)
@ -127,7 +129,6 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
// Set inverter limit relative persistent
payload_val = min<uint32_t>(100, payload_val);
Serial.printf("Limit Persistent: %d %%\n", payload_val);
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)) {
// Set inverter limit relative non persistent
payload_val = min<uint32_t>(100, payload_val);
Serial.printf("Limit Non-Persistent: %d %%\n", payload_val);
if (!properties.retain) {
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent);
@ -155,10 +155,19 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie
Serial.println("Ignored because retained");
}
} else if(!strcmp(setting, TOPIC_SUB_POWER)) {
} else if (!strcmp(setting, TOPIC_SUB_POWER)) {
// Turn inverter on or off
Serial.printf("Set inverter power to: %d\n", payload_val);
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");
}
}
}

View File

@ -115,7 +115,7 @@ void NetworkSettingsClass::setupMode()
WiFi.mode(WIFI_AP_STA);
String ssidString = getApName();
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->start(DNS_PORT, "*", WiFi.softAPIP());
dnsServerStatus = true;

View File

@ -28,6 +28,7 @@ void WebApiClass::init()
_webApiNetwork.init(&_server);
_webApiNtp.init(&_server);
_webApiPower.init(&_server);
_webApiSecurity.init(&_server);
_webApiSysstatus.init(&_server);
_webApiWebapp.init(&_server);
_webApiWsLive.init(&_server);
@ -50,6 +51,7 @@ void WebApiClass::loop()
_webApiNetwork.loop();
_webApiNtp.loop();
_webApiPower.loop();
_webApiSecurity.loop();
_webApiSysstatus.loop();
_webApiWebapp.loop();
_webApiWsLive.loop();

View File

@ -32,7 +32,7 @@ void WebApiConfigClass::loop()
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)
@ -87,7 +87,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
response->setLength();
request->send(response);
LittleFS.remove(CONFIG_FILENAME);
LittleFS.remove(CONFIG_FILENAME_JSON);
ESP.restart();
}
@ -110,7 +110,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi
{
if (!index) {
// 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) {

View File

@ -36,6 +36,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
root[buffer]["max_power"] = inv->DevInfo()->getMaxPower();
LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess();
String limitStatus = "Unknown";

View File

@ -170,6 +170,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
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) {
retMsg[F("message")] = F("Port must be a number between 1 and 65535!");
response->setLength();

View File

@ -151,11 +151,13 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
request->send(response);
return;
}
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!");
response->setLength();
request->send(response);
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) {
retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!");
response->setLength();
request->send(response);
return;
}
}
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!");

View File

@ -39,11 +39,9 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
String limitStatus = "Unknown";
if (status == LastCommandSuccess::CMD_OK) {
limitStatus = "Ok";
}
else if (status == LastCommandSuccess::CMD_NOK) {
} else if (status == LastCommandSuccess::CMD_NOK) {
limitStatus = "Failure";
}
else if (status == LastCommandSuccess::CMD_PENDING) {
} else if (status == LastCommandSuccess::CMD_PENDING) {
limitStatus = "Pending";
}
root[buffer]["power_set_status"] = limitStatus;
@ -86,7 +84,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
}
if (!(root.containsKey("serial")
&& root.containsKey("power"))) {
&& (root.containsKey("power") || root.containsKey("restart")))) {
retMsg[F("message")] = F("Values are missing!");
response->setLength();
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);
uint16_t power = root[F("power")].as<bool>();
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv == nullptr) {
retMsg[F("message")] = F("Invalid inverter specified!");
@ -111,7 +107,14 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
return;
}
inv->sendPowerControlRequest(Hoymiles.getRadio(), power);
if (root.containsKey("power")) {
uint16_t power = root[F("power")].as<bool>();
inv->sendPowerControlRequest(Hoymiles.getRadio(), power);
} else {
if (root[F("restart")].as<bool>()) {
inv->sendRestartControlRequest(Hoymiles.getRadio());
}
}
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");

92
src/WebApi_security.cpp Normal file
View 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);
}

View File

@ -66,6 +66,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root[F("uptime")] = esp_timer_get_time() / 1000000;
root[F("radio_connected")] = Hoymiles.getRadio()->isConnected();
root[F("radio_pvariant")] = Hoymiles.getRadio()->isPVariant();
response->setLength();
request->send(response);

View File

@ -87,6 +87,12 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
root[i][F("reachable")] = inv->isReachable();
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
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {

View File

@ -21,7 +21,7 @@
"@babel/core": "^7.19.3",
"@babel/eslint-parser": "^7.19.1",
"@types/bootstrap": "^5.2.5",
"@types/node": "^18.8.2",
"@types/node": "^18.8.3",
"@types/spark-md5": "^3.0.2",
"@typescript-eslint/parser": "^5.38.1",
"@vue/cli-plugin-babel": "~5.0.8",
@ -30,7 +30,7 @@
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.2",
"eslint": "^8.24.0",
"eslint": "^8.25.0",
"eslint-plugin-vue": "^9.6.0",
"typescript": "^4.8.4",
"vue-cli-plugin-compression": "~2.0.0"

View File

@ -99,3 +99,23 @@
</div>
</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>

View File

@ -55,7 +55,7 @@
<div v-else-if="!uploading">
<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>
@ -116,11 +116,19 @@
<script lang="ts">
import { defineComponent } from 'vue';
import {
BIconExclamationCircleFill,
BIconArrowLeft,
BIconCheckCircle
} from 'bootstrap-icons-vue';
import * as bootstrap from 'bootstrap';
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
export default defineComponent({
components: {
BIconExclamationCircleFill,
BIconArrowLeft,
BIconCheckCircle,
BootstrapAlert,
},
data() {
@ -175,7 +183,7 @@ export default defineComponent({
downloadConfig() {
const link = document.createElement('a')
link.href = "/api/config/get"
link.download = 'config.bin'
link.download = 'config.json'
link.click()
},
uploadConfig(event: Event | null) {

View File

@ -74,8 +74,20 @@
<script lang="ts">
import { defineComponent } from 'vue';
import SparkMD5 from "spark-md5";
import {
BIconExclamationCircleFill,
BIconArrowLeft,
BIconArrowRepeat,
BIconCheckCircle
} from 'bootstrap-icons-vue';
export default defineComponent({
components: {
BIconExclamationCircleFill,
BIconArrowLeft,
BIconArrowRepeat,
BIconCheckCircle,
},
data() {
return {
loading: true,
@ -122,7 +134,7 @@ export default defineComponent({
this.uploading = true;
const formData = new FormData();
if (event !== null) {
const target= event.target as HTMLInputElement;
const target = event.target as HTMLInputElement;
if (target.files !== null) {
this.file = target.files[0];
}

View File

@ -38,11 +38,26 @@
'bg-warning': inverter.reachable && !inverter.producing,
'bg-primary': inverter.reachable && inverter.producing,
}">
{{ inverter.name }} (Inverter Serial Number:
{{ inverter.serial }}) (Data Age:
{{ inverter.data_age }} seconds)
<div class="btn-toolbar" role="toolbar">
<div class="p-2 flex-grow-1">
<div class="d-flex flex-wrap">
<div style="padding-right: 2em;">
{{ inverter.name }}
</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">
<button type="button" class="btn btn-sm btn-danger"
@click="onShowLimitSettings(inverter.serial)"
@ -175,13 +190,22 @@
<div class="row mb-3">
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">Current
Limit:</label>
<div class="col-sm-9">
<div class="col-sm-4">
<div class="input-group">
<input type="number" class="form-control" id="inputCurrentLimit"
aria-describedby="currentLimitType" v-model="currentLimit" disabled />
<span class="input-group-text" id="currentLimitType">%</span>
</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 class="row mb-3 align-items-center">
@ -282,6 +306,9 @@
<button type="button" class="btn btn-danger" @click="onSetPowerSettings(false)">
<BIconToggleOff class="fs-4" />&nbsp;Turn Off
</button>
<button type="button" class="btn btn-warning" @click="onSetPowerSettings(true, true)">
<BIconArrowCounterclockwise class="fs-4" />&nbsp;Restart
</button>
</div>
</template>
@ -298,11 +325,23 @@
<script lang="ts">
import { defineComponent } from 'vue';
import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
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 DevInfo from '@/components/partials/DevInfo.vue';
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
import VedirectView from '@/components/partials/VedirectView.vue';
declare interface Inverter {
@ -310,6 +349,8 @@ declare interface Inverter {
name: string,
reachable: boolean,
producing: boolean,
limit_relative: 0,
limit_absolute: 0,
data_age: 0,
events: 0
}
@ -320,6 +361,16 @@ export default defineComponent({
EventLog,
DevInfo,
BootstrapAlert,
BIconXCircleFill,
BIconExclamationCircleFill,
BIconCheckCircleFill,
BIconSpeedometer,
BIconPower,
BIconCpu,
BIconJournalText,
BIconToggleOn,
BIconToggleOff,
BIconArrowCounterclockwise,
VedirectView
},
data() {
@ -342,7 +393,9 @@ export default defineComponent({
limitSettingLoading: true,
currentLimit: 0,
currentLimitAbsolute: 0,
successCommandLimit: "",
maxPower: 0,
targetLimit: 0,
targetLimitMin: 10,
targetLimitMax: 100,
@ -495,7 +548,11 @@ export default defineComponent({
fetch("/api/limit/status")
.then((response) => response.json())
.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.limitSettingSerial = serial;
this.limitSettingLoading = false;
@ -572,11 +629,20 @@ export default defineComponent({
this.showAlertPower = false;
},
onSetPowerSettings(turnOn: boolean) {
const data = {
serial: this.powerSettingSerial,
power: turnOn,
};
onSetPowerSettings(turnOn: boolean, restart = false) {
let data = {};
if (restart) {
data = {
serial: this.powerSettingSerial,
restart: true,
};
} else {
data = {
serial: this.powerSettingSerial,
power: turnOn,
};
}
const formData = new FormData();
formData.append("data", JSON.stringify(data));

View File

@ -96,7 +96,7 @@
<div class="mb-3" v-for="(max, index) in editInverterData.max_power" :key="`${index}`">
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +
1
1
}}:</label>
<div class="input-group">
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
@ -146,6 +146,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import {
BIconTrash,
BIconPencil
} from 'bootstrap-icons-vue';
import * as bootstrap from 'bootstrap';
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
@ -160,6 +164,8 @@ declare interface Inverter {
export default defineComponent({
components: {
BootstrapAlert,
BIconTrash,
BIconPencil,
},
data() {
return {
@ -184,7 +190,7 @@ export default defineComponent({
this.getInverters();
},
computed: {
sortedInverters() {
sortedInverters(): Inverter[] {
return this.inverters.slice().sort((a, b) => {
return a.serial - b.serial;
});

View File

@ -30,6 +30,10 @@
<router-link @click="onClick" class="dropdown-item" to="/settings/inverter">Inverter Settings
</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/security">Security Settings
</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/dtu">DTU Settings</router-link>
</li>

View 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>

View File

@ -67,6 +67,7 @@ export default defineComponent({
sketch_used: 0,
// RadioInfo
radio_connected: false,
radio_pvariant: false,
}
}
},

View File

@ -10,7 +10,8 @@
<td>Model</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
<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>
<td>Bootloader Version</td>
@ -38,6 +39,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { BIconInfoSquare } from 'bootstrap-icons-vue';
import BootstrapAlert from '@/components/partials/BootstrapAlert.vue';
declare interface DevInfoData {
@ -52,6 +54,7 @@ declare interface DevInfoData {
export default defineComponent({
components: {
BIconInfoSquare,
BootstrapAlert,
},
props: {

View File

@ -52,9 +52,6 @@ export default defineComponent({
const dMins = minutes > 9 ? minutes : "0" + minutes;
const dSecs = seconds > 9 ? seconds : "0" + seconds;
if (days > 0) {
return days + " " + dHours + ":" + dMins + ":" + dSecs;
}
return dHours + ":" + dMins + ":" + dSecs;
};
},

View File

@ -17,6 +17,18 @@
<span v-else>not connected</span>
</td>
</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>
</table>
</div>
@ -30,6 +42,7 @@ import { defineComponent } from 'vue';
export default defineComponent({
props: {
radio_connected: { type: Boolean, required: true },
radio_pvariant: { type: Boolean, required: true },
},
});
</script>

View File

@ -1,9 +1,8 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { BootstrapIconsPlugin } from 'bootstrap-icons-vue';
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap"
createApp(App).use(router).use(BootstrapIconsPlugin).mount('#app')
createApp(App).use(router).mount('#app')

View File

@ -14,6 +14,7 @@ import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
import ConfigAdminView from '@/components/ConfigAdminView.vue'
import VedirectAdminView from '@/components/VedirectAdminView.vue'
import VedirectInfoView from '@/components/VedirectInfoView.vue'
import SecurityAdminView from '@/components/SecurityAdminView.vue'
const routes: Array<RouteRecordRaw> = [
{
@ -90,6 +91,11 @@ const routes: Array<RouteRecordRaw> = [
path: '/settings/config',
name: 'Config Management',
component: ConfigAdminView
},
{
path: '/settings/security',
name: 'Security',
component: SecurityAdminView
}
];

View File

@ -1164,10 +1164,10 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@eslint/eslintrc@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356"
integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==
"@eslint/eslintrc@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@ -1200,11 +1200,6 @@
debug "^4.1.1"
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":
version "1.0.1"
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"
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
"@types/node@^18.8.2":
version "18.8.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067"
integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==
"@types/node@^18.8.3":
version "18.8.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@ -3410,14 +3405,13 @@ eslint-webpack-plugin@^3.1.0:
normalize-path "^3.0.0"
schema-utils "^3.1.1"
eslint@^8.24.0:
version "8.24.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8"
integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==
eslint@^8.25.0:
version "8.25.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.25.0.tgz#00eb962f50962165d0c4ee3327708315eaa8058b"
integrity sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==
dependencies:
"@eslint/eslintrc" "^1.3.2"
"@eslint/eslintrc" "^1.3.3"
"@humanwhocodes/config-array" "^0.10.5"
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
"@humanwhocodes/module-importer" "^1.0.1"
ajv "^6.10.0"
chalk "^4.0.0"