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

This commit is contained in:
helgeerbe 2023-01-20 14:54:04 +01:00
commit 9a7a0d293e
55 changed files with 1186 additions and 221 deletions

View File

@ -66,7 +66,7 @@ Sends text raw data as difined in VE.Direct spec.
**TSUN compatibility remark:**
Compatibility with OpenDTU seems to be related to serial numbers. Current findings indicate that TSUN inverters with a serial number starting with "11" are supported, whereby inverters with a serial number starting with "10" are not.
Firmware version seems to play not a significant role and cannot be read from the stickers. For completeness, the following firmware version have been reported to work with OpenDTU:
* v1.0.10 TSOL-M800 (DE)
* v1.0.8, v1.0.10 TSOL-M800 (DE)
* v1.0.12 TSOL-M1600
## Features for end users
@ -86,7 +86,7 @@ Firmware version seems to play not a significant role and cannot be read from th
* Ve.Direct interface (via web-interface, REST-api, or MQTT)
* Ethernet support
* Prometheus API endpoint (/api/prometheus/metrics)
* English and german web interface
* English, german and french web interface
## Features for developers
* The microcontroller part
@ -151,6 +151,7 @@ This can be achieved by copying one of the [env:....] sections from 'platformio.
-DVICTRON_PIN_RX=22
```
It is recommended to make all changes only in the 'platformio_override.ini', this is your personal copy.
You can also change the pins by creating a custom [device profile](docs/DeviceProfiles.md).
## Flashing and starting up
### with Visual Studio Code
@ -216,6 +217,9 @@ esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_
#### Flash with ESP_Flasher (Windows)
Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/releases/) is suitable for flashing OpenDTU on Windows.
#### Flash with [ESP_Flasher](https://espressif.github.io/esptool-js/) - web version
It is also possible to flash it via the web tools which might be more convenient and is platformindependent.
## First configuration
* After the initial flashing of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42".
* Use a web browser to open the address [http://192.168.4.1](http://192.168.4.1)

56
docs/DeviceProfiles.md Normal file
View File

@ -0,0 +1,56 @@
# Device Profiles
It is possible to change hardware settings like pin assignments or ethernet support using a json file. The json file can be uploaded using the configuration management in the web interface. Just select "Pin Mapping (pin_mapping.json)" in the recovery section.
When the file is uploaded the ESP performs a reboot. This is required as the pin settings could have changed within the file. By default all the pin assignments are used as compiled into the firmware.
To change the device profile, navigate to the "Device Manager" and selected the appropriate profile. You can see the current (Active) and the new (Selected) in assignment in the table below the combobox.
## Structure of the json file
```json
[
{
"name": "Generic NodeMCU 38 pin",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 4,
"cs": 5
},
"eth": {
"enabled": false,
"phy_addr": -1,
"power": -1,
"mdc": -1,
"mdio": -1,
"type": -1,
"clk_mode": -1
}
},
{
"name": "Olimex ESP32-POE",
"nrf24": {
"miso": 15,
"mosi": 2,
"clk": 14,
"irq": 13,
"en": 16,
"cs": 5
},
"eth": {
"enabled": true,
"phy_addr": 0,
"power": 12,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 3
}
}
]
```
The json file can contain multiple profiles. Each profile requires a name and different parameters. If one parameter is not set, the default value, as compiled into the firmware is used. The example above shows all the currently supported values. Others may follow. A sample file with a lot of boards can be found [here](pin_mapping.json). This means you can just flash the generic bin file and upload the pin_mapping.json file. Then you select your board and everything works hopyfully as expected.

View File

@ -4,4 +4,5 @@ More detailed descriptions for some topics can be found here.
## [MQTT Topic Documentation](MQTT_Topics.md)
## [Web API Documentation](Web-API.md)
## [Device Profile Documentation](DeviceProfiles.md)
## [Builds](builds/README.md)

122
docs/pin_mapping.json Normal file
View File

@ -0,0 +1,122 @@
[
{
"name": "Generic NodeMCU 38 pin",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 4,
"cs": 5
},
"eth": {
"enabled": false,
"phy_addr": -1,
"power": -1,
"mdc": -1,
"mdio": -1,
"type": -1,
"clk_mode": -1
}
},
{
"name": "Olimex ESP32-POE",
"nrf24": {
"miso": 15,
"mosi": 2,
"clk": 14,
"irq": 13,
"en": 16,
"cs": 5
},
"eth": {
"enabled": true,
"phy_addr": 0,
"power": 12,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 3
}
},
{
"name": "Olimex ESP32-EVB",
"nrf24": {
"miso": 15,
"mosi": 2,
"clk": 14,
"irq": 13,
"en": 16,
"cs": 17
},
"eth": {
"enabled": true,
"phy_addr": 0,
"power": 12,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 0
}
},
{
"name": "Generic NodeMCU 30 pin",
"nrf24": {
"miso": 19,
"mosi": 23,
"clk": 18,
"irq": 16,
"en": 17,
"cs": 5
},
"eth": {
"enabled": false,
"phy_addr": -1,
"power": -1,
"mdc": -1,
"mdio": -1,
"type": -1,
"clk_mode": -1
}
},
{
"name": "WT32-ETH01",
"nrf24": {
"miso": 4,
"mosi": 2,
"clk": 32,
"irq": 33,
"en": 14,
"cs": 15
},
"eth": {
"enabled": true,
"phy_addr": 1,
"power": 16,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 3
}
},
{
"name": "LILYGO TTGO T-Internet-POE",
"nrf24": {
"miso": 2,
"mosi": 15,
"clk": 14,
"irq": 34,
"en": 12,
"cs": 4
},
"eth": {
"enabled": true,
"phy_addr": 0,
"power": -1,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 3
}
}
]

View File

@ -19,7 +19,7 @@
#define MQTT_MAX_PASSWORD_STRLEN 64
#define MQTT_MAX_TOPIC_STRLEN 32
#define MQTT_MAX_LWTVALUE_STRLEN 20
#define MQTT_MAX_ROOT_CA_CERT_STRLEN 2048
#define MQTT_MAX_ROOT_CA_CERT_STRLEN 2560
#define INV_MAX_NAME_STRLEN 31
#define INV_MAX_COUNT 10
@ -27,6 +27,8 @@
#define CHAN_MAX_NAME_STRLEN 31
#define DEV_MAX_MAPPING_NAME_STRLEN 31
#define JSON_BUFFER_SIZE 6144
struct CHANNEL_CONFIG_T {
@ -92,6 +94,8 @@ struct CONFIG_T {
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
bool Security_AllowReadonly;
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
};
class ConfigurationClass {

42
include/PinMapping.h Normal file
View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <stdint.h>
#include <ETH.h>
#define PINMAPPING_FILENAME "/pin_mapping.json"
#define MAPPING_NAME_STRLEN 31
struct PinMapping_t {
char name[MAPPING_NAME_STRLEN + 1];
int8_t nrf24_miso;
int8_t nrf24_mosi;
int8_t nrf24_clk;
int8_t nrf24_irq;
int8_t nrf24_en;
int8_t nrf24_cs;
int8_t eth_phy_addr;
bool eth_enabled;
int eth_power;
int eth_mdc;
int eth_mdio;
eth_phy_type_t eth_type;
eth_clock_mode_t eth_clk_mode;
};
class PinMappingClass {
public:
PinMappingClass();
bool init(const String& deviceMapping);
PinMapping_t& get();
bool isValidNrf24Config();
bool isValidEthConfig();
private:
PinMapping_t _pinMapping;
};
extern PinMappingClass PinMapping;

View File

@ -6,6 +6,7 @@
#include "WebApi_dtu.h"
#include "WebApi_eventlog.h"
#include "WebApi_firmware.h"
#include "WebApi_device.h"
#include "WebApi_inverter.h"
#include "WebApi_limit.h"
#include "WebApi_maintenance.h"
@ -37,6 +38,7 @@ private:
AsyncEventSource _events;
WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu;
WebApiEventlogClass _webApiEventlog;

View File

@ -11,6 +11,7 @@ public:
private:
void onConfigGet(AsyncWebServerRequest* request);
void onConfigDelete(AsyncWebServerRequest* request);
void onConfigListGet(AsyncWebServerRequest* request);
void onConfigUploadFinish(AsyncWebServerRequest* request);
void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);

16
include/WebApi_device.h Normal file
View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiDeviceClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onDeviceAdminGet(AsyncWebServerRequest* request);
void onDeviceAdminPost(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -81,4 +81,7 @@ enum WebApiError {
PowerBase = 11000,
PowerSerialZero,
PowerInvalidInverter,
HardwareBase = 12000,
HardwarePinMappingLength,
};

View File

@ -79,6 +79,8 @@
#define MQTT_HASS_TOPIC "homeassistant/"
#define MQTT_HASS_INDIVIDUALPANELS false
#define DEV_PINMAPPING ""
#define VEDIRECT_ENABLED false
#define VEDIRECT_UPDATESONLY true
#define VEDIRECT_POLL_INTERVAL 5

View File

@ -108,12 +108,12 @@ void InverterAbstract::clearRxFragmentBuffer()
void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len)
{
if (len < 11) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__);
return;
}
if (len - 11 > MAX_RF_PAYLOAD_SIZE) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too large\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too large\r\n", __FILE__, __LINE__);
return;
}

View File

@ -15,7 +15,7 @@ void AlarmLogParser::clearBuffer()
void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len)
{
if (offset + len > ALARM_LOG_PAYLOAD_SIZE) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer (%d > %d)\r\n", __FILE__, __LINE__, offset + len, ALARM_LOG_PAYLOAD_SIZE);
return;
}
memcpy(&_payloadAlarmLog[offset], payload, len);

View File

@ -37,7 +37,7 @@ void DevInfoParser::clearBufferAll()
void DevInfoParser::appendFragmentAll(uint8_t offset, uint8_t* payload, uint8_t len)
{
if (offset + len > DEV_INFO_SIZE) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info all packet too large for buffer\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info all packet too large for buffer\r\n", __FILE__, __LINE__);
return;
}
memcpy(&_payloadDevInfoAll[offset], payload, len);
@ -53,7 +53,7 @@ void DevInfoParser::clearBufferSimple()
void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len)
{
if (offset + len > DEV_INFO_SIZE) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) dev info Simple packet too large for buffer\r\n", __FILE__, __LINE__);
return;
}
memcpy(&_payloadDevInfoSimple[offset], payload, len);

View File

@ -43,7 +43,7 @@ void StatisticsParser::clearBuffer()
void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len)
{
if (offset + len > STATISTIC_PACKET_SIZE) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__);
return;
}
memcpy(&_payloadStatistic[offset], payload, len);

View File

@ -15,7 +15,7 @@ void SystemConfigParaParser::clearBuffer()
void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len)
{
if (offset + len > (SYSTEM_CONFIG_PARA_SIZE)) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__);
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) stats packet too large for buffer\r\n", __FILE__, __LINE__);
return;
}
memcpy(&_payload[offset], payload, len);

View File

@ -34,15 +34,19 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id)
case 3:
reason_str = F("Software reset digital core");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 4:
reason_str = F("Legacy watch dog reset digital core");
break;
#endif
case 5:
reason_str = F("Deep Sleep reset digital core");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 6:
reason_str = F("Reset by SLC module, reset digital core");
break;
#endif
case 7:
reason_str = F("Timer Group0 Watch dog reset digital core");
break;
@ -64,9 +68,11 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id)
case 13:
reason_str = F("RTC Watch dog Reset CPU");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 14:
reason_str = F("for APP CPU, reseted by PRO CPU");
break;
#endif
case 15:
reason_str = F("Reset when the vdd voltage is not stable");
break;
@ -94,15 +100,19 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id)
case 3:
reason_str = F("SW_RESET");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 4:
reason_str = F("OWDT_RESET");
break;
#endif
case 5:
reason_str = F("DEEPSLEEP_RESET");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 6:
reason_str = F("SDIO_RESET");
break;
#endif
case 7:
reason_str = F("TG0WDT_SYS_RESET");
break;
@ -124,9 +134,11 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id)
case 13:
reason_str = F("RTCWDT_CPU_RESET");
break;
#ifndef CONFIG_IDF_TARGET_ESP32C3
case 14:
reason_str = F("EXT_CPU_RESET");
break;
#endif
case 15:
reason_str = F("RTCWDT_BROWN_OUT_RESET");
break;

View File

@ -15,16 +15,15 @@ extra_configs =
[env]
framework = arduino
platform = espressif32@>=5.3.0
platform = espressif32@>=6.0.0
build_flags =
-D=${PIOENV}
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
-Wall -Wextra -Werror
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.19.4
bblanchon/ArduinoJson @ ^6.20.0
https://github.com/bertmelis/espMqttClient.git#v1.3.3
nrf24/RF24 @ ^1.4.5

View File

@ -80,6 +80,9 @@ bool ConfigurationClass::write()
security["password"] = config.Security_Password;
security["allow_readonly"] = config.Security_AllowReadonly;
JsonObject device = doc.createNestedObject("device");
device["pinmapping"] = config.Dev_PinMapping;
JsonArray inverters = doc.createNestedArray("inverters");
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters.createNestedObject();
@ -206,6 +209,9 @@ bool ConfigurationClass::read()
strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password));
config.Security_AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY;
JsonObject device = doc["device"];
strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping));
JsonArray inverters = doc["inverters"];
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters[i].as<JsonObject>();

View File

@ -186,17 +186,17 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
// Set inverter limit relative persistent
MessageOutput.printf("Limit Persistent: %d %%\n", payload_val);
MessageOutput.printf("Limit Persistent: %d %%\r\n", payload_val);
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent);
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) {
// Set inverter limit absolute persistent
MessageOutput.printf("Limit Persistent: %d W\n", payload_val);
MessageOutput.printf("Limit Persistent: %d W\r\n", payload_val);
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutPersistent);
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) {
// Set inverter limit relative non persistent
MessageOutput.printf("Limit Non-Persistent: %d %%\n", payload_val);
MessageOutput.printf("Limit Non-Persistent: %d %%\r\n", payload_val);
if (!properties.retain) {
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent);
} else {
@ -205,7 +205,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) {
// Set inverter limit absolute non persistent
MessageOutput.printf("Limit Non-Persistent: %d W\n", payload_val);
MessageOutput.printf("Limit Non-Persistent: %d W\r\n", payload_val);
if (!properties.retain) {
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::AbsolutNonPersistent);
} else {
@ -214,12 +214,12 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else if (!strcmp(setting, TOPIC_SUB_POWER)) {
// Turn inverter on or off
MessageOutput.printf("Set inverter power to: %d\n", payload_val);
MessageOutput.printf("Set inverter power to: %d\r\n", payload_val);
inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0);
} else if (!strcmp(setting, TOPIC_SUB_RESTART)) {
// Restart inverter
MessageOutput.printf("Restart inverter\n");
MessageOutput.printf("Restart inverter\r\n");
if (!properties.retain && payload_val == 1) {
inv->sendRestartControlRequest(Hoymiles.getRadio());
} else {

View File

@ -5,11 +5,10 @@
#include "NetworkSettings.h"
#include "Configuration.h"
#include "MessageOutput.h"
#include "PinMapping.h"
#include "Utils.h"
#include "defaults.h"
#ifdef OPENDTU_ETHERNET
#include <ETH.h>
#endif
NetworkSettingsClass::NetworkSettingsClass()
: apIp(192, 168, 4, 1)
@ -29,7 +28,6 @@ void NetworkSettingsClass::init()
void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
{
switch (event) {
#ifdef OPENDTU_ETHERNET
case ARDUINO_EVENT_ETH_START:
MessageOutput.println(F("ETH start"));
if (_networkMode == network_mode::Ethernet) {
@ -48,7 +46,7 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
raiseEvent(network_event::NETWORK_CONNECTED);
break;
case ARDUINO_EVENT_ETH_GOT_IP:
MessageOutput.printf("ETH got IP: %s\n", ETH.localIP().toString().c_str());
MessageOutput.printf("ETH got IP: %s\r\n", ETH.localIP().toString().c_str());
if (_networkMode == network_mode::Ethernet) {
raiseEvent(network_event::NETWORK_GOT_IP);
}
@ -60,7 +58,6 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
raiseEvent(network_event::NETWORK_DISCONNECTED);
}
break;
#endif
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
MessageOutput.println(F("WiFi connected"));
if (_networkMode == network_mode::WiFi) {
@ -76,7 +73,7 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
}
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
MessageOutput.printf("WiFi got ip: %s\n", WiFi.localIP().toString().c_str());
MessageOutput.printf("WiFi got ip: %s\r\n", WiFi.localIP().toString().c_str());
if (_networkMode == network_mode::WiFi) {
raiseEvent(network_event::NETWORK_GOT_IP);
}
@ -129,9 +126,11 @@ void NetworkSettingsClass::setupMode()
WiFi.mode(WIFI_MODE_NULL);
}
}
#ifdef OPENDTU_ETHERNET
ETH.begin();
#endif
if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get();
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
}
}
void NetworkSettingsClass::enableAdminMode()
@ -148,7 +147,6 @@ String NetworkSettingsClass::getApName()
void NetworkSettingsClass::loop()
{
#ifdef OPENDTU_ETHERNET
if (_ethConnected) {
if (_networkMode != network_mode::Ethernet) {
// Do stuff when switching to Ethernet mode
@ -159,7 +157,6 @@ void NetworkSettingsClass::loop()
setHostname();
}
} else
#endif
if (_networkMode != network_mode::WiFi) {
// Do stuff when switching to Ethernet mode
MessageOutput.println(F("Switch to WiFi mode"));
@ -250,7 +247,6 @@ void NetworkSettingsClass::setHostname()
WiFi.mode(WIFI_MODE_STA);
setupMode();
}
#ifdef OPENDTU_ETHERNET
else if (_networkMode == network_mode::Ethernet) {
if (ETH.setHostname(getHostname().c_str())) {
MessageOutput.println(F("done"));
@ -258,7 +254,6 @@ void NetworkSettingsClass::setHostname()
MessageOutput.println(F("failed"));
}
}
#endif
}
void NetworkSettingsClass::setStaticIp()
@ -279,7 +274,6 @@ void NetworkSettingsClass::setStaticIp()
MessageOutput.println(F("done"));
}
}
#ifdef OPENDTU_ETHERNET
else if (_networkMode == network_mode::Ethernet) {
if (Configuration.get().WiFi_Dhcp) {
MessageOutput.print(F("Configuring Ethernet DHCP IP... "));
@ -296,17 +290,14 @@ void NetworkSettingsClass::setStaticIp()
MessageOutput.println(F("done"));
}
}
#endif
}
IPAddress NetworkSettingsClass::localIP()
{
switch (_networkMode) {
#ifdef OPENDTU_ETHERNET
case network_mode::Ethernet:
return ETH.localIP();
break;
#endif
case network_mode::WiFi:
return WiFi.localIP();
break;
@ -318,11 +309,9 @@ IPAddress NetworkSettingsClass::localIP()
IPAddress NetworkSettingsClass::subnetMask()
{
switch (_networkMode) {
#ifdef OPENDTU_ETHERNET
case network_mode::Ethernet:
return ETH.subnetMask();
break;
#endif
case network_mode::WiFi:
return WiFi.subnetMask();
break;
@ -334,11 +323,9 @@ IPAddress NetworkSettingsClass::subnetMask()
IPAddress NetworkSettingsClass::gatewayIP()
{
switch (_networkMode) {
#ifdef OPENDTU_ETHERNET
case network_mode::Ethernet:
return ETH.gatewayIP();
break;
#endif
case network_mode::WiFi:
return WiFi.gatewayIP();
break;
@ -350,11 +337,9 @@ IPAddress NetworkSettingsClass::gatewayIP()
IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no)
{
switch (_networkMode) {
#ifdef OPENDTU_ETHERNET
case network_mode::Ethernet:
return ETH.dnsIP(dns_no);
break;
#endif
case network_mode::WiFi:
return WiFi.dnsIP(dns_no);
break;
@ -366,11 +351,9 @@ IPAddress NetworkSettingsClass::dnsIP(uint8_t dns_no)
String NetworkSettingsClass::macAddress()
{
switch (_networkMode) {
#ifdef OPENDTU_ETHERNET
case network_mode::Ethernet:
return ETH.macAddress();
break;
#endif
case network_mode::WiFi:
return WiFi.macAddress();
break;
@ -420,11 +403,7 @@ String NetworkSettingsClass::getHostname()
bool NetworkSettingsClass::isConnected()
{
#ifndef OPENDTU_ETHERNET
return WiFi.localIP()[0] != 0;
#else
return WiFi.localIP()[0] != 0 || ETH.localIP()[0] != 0;
#endif
}
network_mode NetworkSettingsClass::NetworkMode()

103
src/PinMapping.cpp Normal file
View File

@ -0,0 +1,103 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 - 2023 Thomas Basler and others
*/
#include "PinMapping.h"
#include "MessageOutput.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <string.h>
#define JSON_BUFFER_SIZE 6144
PinMappingClass PinMapping;
PinMappingClass::PinMappingClass()
{
memset(&_pinMapping, 0x0, sizeof(_pinMapping));
_pinMapping.nrf24_clk = HOYMILES_PIN_SCLK;
_pinMapping.nrf24_cs = HOYMILES_PIN_CS;
_pinMapping.nrf24_en = HOYMILES_PIN_CE;
_pinMapping.nrf24_irq = HOYMILES_PIN_IRQ;
_pinMapping.nrf24_miso = HOYMILES_PIN_MISO;
_pinMapping.nrf24_mosi = HOYMILES_PIN_MOSI;
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = true;
#else
_pinMapping.eth_enabled = false;
#endif
_pinMapping.eth_phy_addr = ETH_PHY_ADDR;
_pinMapping.eth_power = ETH_PHY_POWER;
_pinMapping.eth_mdc = ETH_PHY_MDC;
_pinMapping.eth_mdio = ETH_PHY_MDIO;
_pinMapping.eth_type = ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = ETH_CLK_MODE;
}
PinMapping_t& PinMappingClass::get()
{
return _pinMapping;
}
bool PinMappingClass::init(const String& deviceMapping)
{
File f = LittleFS.open(PINMAPPING_FILENAME, "r", false);
if (!f) {
return false;
}
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
// Deserialize the JSON document
DeserializationError error = deserializeJson(doc, f);
if (error) {
MessageOutput.println(F("Failed to read file, using default configuration"));
}
for (uint8_t i = 1; i <= doc.size(); i++) {
String devName = doc[i]["name"] | "";
if (devName == deviceMapping) {
strlcpy(_pinMapping.name, devName.c_str(), sizeof(_pinMapping.name));
_pinMapping.nrf24_clk = doc[i]["nrf24"]["clk"] | HOYMILES_PIN_SCLK;
_pinMapping.nrf24_cs = doc[i]["nrf24"]["cs"] | HOYMILES_PIN_CS;
_pinMapping.nrf24_en = doc[i]["nrf24"]["en"] | HOYMILES_PIN_CE;
_pinMapping.nrf24_irq = doc[i]["nrf24"]["irq"] | HOYMILES_PIN_IRQ;
_pinMapping.nrf24_miso = doc[i]["nrf24"]["miso"] | HOYMILES_PIN_MISO;
_pinMapping.nrf24_mosi = doc[i]["nrf24"]["mosi"] | HOYMILES_PIN_MOSI;
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true;
#else
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false;
#endif
_pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR;
_pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER;
_pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC;
_pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO;
_pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE;
return true;
}
}
return false;
}
bool PinMappingClass::isValidNrf24Config()
{
return _pinMapping.nrf24_clk > 0
&& _pinMapping.nrf24_cs > 0
&& _pinMapping.nrf24_en > 0
&& _pinMapping.nrf24_irq > 0
&& _pinMapping.nrf24_miso > 0
&& _pinMapping.nrf24_mosi > 0;
}
bool PinMappingClass::isValidEthConfig()
{
return _pinMapping.eth_enabled;
}

View File

@ -18,6 +18,7 @@ void WebApiClass::init()
_server.addHandler(&_events);
_webApiConfig.init(&_server);
_webApiDevice.init(&_server);
_webApiDevInfo.init(&_server);
_webApiDtu.init(&_server);
_webApiEventlog.init(&_server);
@ -44,6 +45,7 @@ void WebApiClass::init()
void WebApiClass::loop()
{
_webApiConfig.loop();
_webApiDevice.loop();
_webApiDevInfo.loop();
_webApiDtu.loop();
_webApiEventlog.loop();

View File

@ -22,6 +22,7 @@ void WebApiConfigClass::init(AsyncWebServer* server)
_server->on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1));
_server->on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1));
_server->on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1));
_server->on("/api/config/upload", HTTP_POST,
std::bind(&WebApiConfigClass::onConfigUploadFinish, this, _1),
std::bind(&WebApiConfigClass::onConfigUpload, this, _1, _2, _3, _4, _5, _6));
@ -37,7 +38,17 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
return;
}
request->send(LittleFS, CONFIG_FILENAME, String(), true);
String requestFile = CONFIG_FILENAME;
if (request->hasParam("file")) {
String name = "/" + request->getParam("file")->value();
if (LittleFS.exists(name)) {
requestFile = name;
} else {
request->send(404);
}
}
request->send(LittleFS, requestFile, String(), true);
}
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
@ -106,6 +117,33 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
ESP.restart();
}
void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot();
JsonArray data = root.createNestedArray(F("configs"));
File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile();
while (file) {
if (file.isDirectory()) {
continue;
}
JsonObject obj = data.createNestedObject();
obj["name"] = String(file.name());
file = rootfs.openNextFile();
}
file.close();
response->setLength();
request->send(response);
}
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
@ -133,7 +171,12 @@ 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");
if (!request->hasParam("file")) {
request->send(500);
return;
}
String name = "/" + request->getParam("file")->value();
request->_tempFile = LittleFS.open(name, "w");
}
if (len) {

133
src/WebApi_device.cpp Normal file
View File

@ -0,0 +1,133 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "WebApi_device.h"
#include "Configuration.h"
#include "PinMapping.h"
#include "WebApi.h"
#include "WebApi_errors.h"
#include "helper.h"
#include <AsyncJson.h>
void WebApiDeviceClass::init(AsyncWebServer* server)
{
using std::placeholders::_1;
_server = server;
_server->on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1));
_server->on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1));
}
void WebApiDeviceClass::loop()
{
}
void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get();
const PinMapping_t& pin = PinMapping.get();
JsonObject curPin = root.createNestedObject("curPin");
curPin[F("name")] = config.Dev_PinMapping;
JsonObject nrfObj = curPin.createNestedObject("nrf24");
nrfObj[F("clk")] = pin.nrf24_clk;
nrfObj[F("cs")] = pin.nrf24_cs;
nrfObj[F("en")] = pin.nrf24_en;
nrfObj[F("irq")] = pin.nrf24_irq;
nrfObj[F("miso")] = pin.nrf24_miso;
nrfObj[F("mosi")] = pin.nrf24_mosi;
JsonObject ethObj = curPin.createNestedObject("eth");
ethObj[F("enabled")] = pin.eth_enabled;
ethObj[F("phy_addr")] = pin.eth_phy_addr;
ethObj[F("power")] = pin.eth_power;
ethObj[F("mdc")] = pin.eth_mdc;
ethObj[F("mdio")] = pin.eth_mdio;
ethObj[F("type")] = pin.eth_type;
ethObj[F("clk_mode")] = pin.eth_clk_mode;
response->setLength();
request->send(response);
}
void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning");
if (!request->hasParam("data", true)) {
retMsg[F("message")] = F("No values found!");
retMsg[F("code")] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
String json = request->getParam("data", true)->value();
if (json.length() > MQTT_JSON_DOC_SIZE) {
retMsg[F("message")] = F("Data too large!");
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(MQTT_JSON_DOC_SIZE);
DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg[F("message")] = F("Failed to parse data!");
retMsg[F("code")] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("curPin"))) {
retMsg[F("message")] = F("Values are missing!");
retMsg[F("code")] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
return;
}
if (root[F("curPin")][F("name")].as<String>().length() == 0 || root[F("curPin")][F("name")].as<String>().length() > DEV_MAX_MAPPING_NAME_STRLEN) {
retMsg[F("message")] = F("Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!");
retMsg[F("code")] = WebApiError::HardwarePinMappingLength;
retMsg[F("param")][F("max")] = DEV_MAX_MAPPING_NAME_STRLEN;
response->setLength();
request->send(response);
return;
}
CONFIG_T& config = Configuration.get();
strlcpy(config.Dev_PinMapping, root[F("curPin")][F("name")].as<String>().c_str(), sizeof(config.Dev_PinMapping));
Configuration.write();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");
retMsg[F("code")] = WebApiError::GenericSuccess;
response->setLength();
request->send(response);
yield();
delay(1000);
yield();
ESP.restart();
}

View File

@ -12,6 +12,7 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "NtpSettings.h"
#include "PinMapping.h"
#include "Utils.h"
#include "WebApi.h"
#include "defaults.h"
@ -58,6 +59,15 @@ void setup()
}
MessageOutput.println(F("done"));
// Load PinMapping
MessageOutput.print(F("Reading PinMapping... "));
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
MessageOutput.print(F("found valid mapping "));
} else {
MessageOutput.print(F("using default config "));
}
MessageOutput.println(F("done"));
// Initialize WiFi
MessageOutput.print(F("Initialize Network... "));
NetworkSettings.init();
@ -99,39 +109,44 @@ void setup()
// Initialize inverter communication
MessageOutput.print(F("Initialize Hoymiles interface... "));
SPIClass* spiClass = new SPIClass(HSPI);
spiClass->begin(HOYMILES_PIN_SCLK, HOYMILES_PIN_MISO, HOYMILES_PIN_MOSI, HOYMILES_PIN_CS);
Hoymiles.setMessageOutput(&MessageOutput);
Hoymiles.init(spiClass, HOYMILES_PIN_CE, HOYMILES_PIN_IRQ);
if (PinMapping.isValidNrf24Config()) {
SPIClass* spiClass = new SPIClass(HSPI);
PinMapping_t& pin = PinMapping.get();
spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs);
Hoymiles.setMessageOutput(&MessageOutput);
Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq);
MessageOutput.println(F(" Setting radio PA level... "));
Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel);
MessageOutput.println(F(" Setting radio PA level... "));
Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel);
MessageOutput.println(F(" Setting DTU serial... "));
Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial);
MessageOutput.println(F(" Setting DTU serial... "));
Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial);
MessageOutput.println(F(" Setting poll interval... "));
Hoymiles.setPollInterval(config.Dtu_PollInterval);
MessageOutput.println(F(" Setting poll interval... "));
Hoymiles.setPollInterval(config.Dtu_PollInterval);
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) {
MessageOutput.print(F(" Adding inverter: "));
MessageOutput.print(config.Inverter[i].Serial, HEX);
MessageOutput.print(F(" - "));
MessageOutput.print(config.Inverter[i].Name);
auto inv = Hoymiles.addInverter(
config.Inverter[i].Name,
config.Inverter[i].Serial);
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) {
MessageOutput.print(F(" Adding inverter: "));
MessageOutput.print(config.Inverter[i].Serial, HEX);
MessageOutput.print(F(" - "));
MessageOutput.print(config.Inverter[i].Name);
auto inv = Hoymiles.addInverter(
config.Inverter[i].Name,
config.Inverter[i].Serial);
if (inv != nullptr) {
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower);
if (inv != nullptr) {
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower);
}
}
MessageOutput.println(F(" done"));
}
MessageOutput.println(F(" done"));
}
MessageOutput.println(F("done"));
} else {
MessageOutput.println(F("Invalid pin config"));
}
MessageOutput.println(F("done"));
// Initialize ve.direct communication
MessageOutput.println(F("Initialize ve.direct interface... "));

View File

@ -28,14 +28,14 @@
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.31.0",
"eslint-plugin-vue": "^9.8.0",
"eslint": "^8.32.0",
"eslint-plugin-vue": "^9.9.0",
"npm-run-all": "^4.1.5",
"sass": "^1.57.1",
"typescript": "^4.9.4",
"vite": "^4.0.3",
"vite": "^4.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^2.2.0",
"vue-tsc": "^1.0.19"
"vite-plugin-css-injected-by-js": "^2.4.0",
"vue-tsc": "^1.0.24"
}
}

View File

@ -13,7 +13,7 @@
</tr>
<tr>
<td>{{ $t('devinfo.DetectedMaxPower') }}</td>
<td>{{ devInfoList.max_power }} W</td>
<td>{{ $n(devInfoList.max_power, 'decimal') }} W</td>
</tr>
<tr>
<td>{{ $t('devinfo.BootloaderVersion') }}</td>

View File

@ -41,7 +41,7 @@
</tr>
<tr>
<th>{{ $t('firmwareinfo.ConfigSaveCount') }}</th>
<td>{{ systemStatus.cfgsavecount }}</td>
<td>{{ $n(systemStatus.cfgsavecount, 'decimal') }}</td>
</tr>
<tr>
<th>{{ $t('firmwareinfo.Uptime') }}</th>

View File

@ -5,16 +5,15 @@
<div class="progress">
<div class="progress-bar" role="progressbar" :style="{ width: getPercent() + '%' }"
v-bind:aria-valuenow="getPercent()" aria-valuemin="0" aria-valuemax="100">
{{ getPercent() }}%
{{ $n(getPercent() / 100, 'percent') }}
</div>
</div>
</td>
<td class="rightCell">
{{ Math.round((total - used) / 1024) }}
KByte
{{ $n(Math.round((total - used) / 1024), 'kilobyte') }}
</td>
<td class="rightCell">{{ Math.round(used / 1024) }} KByte</td>
<td class="rightCell">{{ Math.round(total / 1024) }} KByte</td>
<td class="rightCell">{{ $n(Math.round(used / 1024), 'kilobyte') }}</td>
<td class="rightCell">{{ $n(Math.round(total / 1024), 'kilobyte') }}</td>
</tr>
</template>

View File

@ -19,7 +19,12 @@
<tr v-for="(property, key) in channelData" :key="`prop-${key}`">
<template v-if="key != 'name' && property">
<th scope="row">{{ $t('inverterchannelproperty.' + key) }}</th>
<td style="text-align: right">{{ formatNumber(property.v, property.d) }}</td>
<td style="text-align: right">
{{ $n(property.v, 'decimal', {
minimumFractionDigits: property.d,
maximumFractionDigits: property.d})
}}
</td>
<td>{{ property.u }}</td>
</template>
</tr>
@ -31,7 +36,6 @@
<script lang="ts">
import type { InverterStatistics } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
import { defineComponent, type PropType } from 'vue';
export default defineComponent({
@ -39,8 +43,5 @@ export default defineComponent({
channelData: { type: Object as PropType<InverterStatistics>, required: true },
channelNumber: { type: Number, required: true },
},
methods: {
formatNumber,
},
});
</script>

View File

@ -4,7 +4,11 @@
<div class="card">
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalYieldTotal') }}</div>
<div class="card-body card-text text-center">
<h2>{{ formatNumber(totalData.YieldTotal.v, totalData.YieldTotal.d) }}
<h2>
{{ $n(totalData.YieldTotal.v, 'decimal', {
minimumFractionDigits: totalData.YieldTotal.d,
maximumFractionDigits: totalData.YieldTotal.d
})}}
<small class="text-muted">{{ totalData.YieldTotal.u }}</small>
</h2>
</div>
@ -14,7 +18,11 @@
<div class="card">
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalYieldDay') }}</div>
<div class="card-body card-text text-center">
<h2>{{ formatNumber(totalData.YieldDay.v, totalData.YieldDay.d) }}
<h2>
{{ $n(totalData.YieldDay.v, 'decimal', {
minimumFractionDigits: totalData.YieldDay.d,
maximumFractionDigits: totalData.YieldDay.d
})}}
<small class="text-muted">{{ totalData.YieldDay.u }}</small>
</h2>
</div>
@ -24,7 +32,11 @@
<div class="card">
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalPower') }}</div>
<div class="card-body card-text text-center">
<h2>{{ formatNumber(totalData.Power.v, totalData.Power.d) }}
<h2>
{{ $n(totalData.Power.v, 'decimal', {
minimumFractionDigits: totalData.Power.d,
maximumFractionDigits: totalData.Power.d
})}}
<small class="text-muted">{{ totalData.Power.u }}</small>
</h2>
</div>
@ -35,15 +47,11 @@
<script lang="ts">
import type { Total } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
import { defineComponent, type PropType } from 'vue';
export default defineComponent({
props: {
totalData: { type: Object as PropType<Total>, required: true },
},
methods: {
formatNumber,
},
});
</script>

View File

@ -1,28 +0,0 @@
<template>
<table class="table table-hover">
<tbody>
<tr>
<td>Current Limit</td>
<td>{{ formatNumber(limitData.limit, 2) }}%</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import { formatNumber } from '@/utils';
import { defineComponent } from 'vue';
declare interface LimitData {
limit: number;
}
export default defineComponent({
props: {
limitData: { type: Object as () => LimitData, required: true },
},
methods: {
formatNumber,
}
});
</script>

View File

@ -1,13 +1,14 @@
<template>
<select class="form-select" @change="updateLanguage()" v-model="$i18n.locale">
<option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale">
{{ locale.toUpperCase() }}
{{ getLocaleName(locale) }}
</option>
</select>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { LOCALES } from '@/locales';
export default defineComponent({
name: "LocaleSwitcher",
@ -15,6 +16,9 @@ export default defineComponent({
updateLanguage() {
localStorage.setItem("locale", this.$i18n.locale);
},
getLocaleName(locale: string): string {
return LOCALES.find(i => i.value === locale)?.caption || "";
}
},
mounted() {
if (localStorage.getItem("locale")) {

View File

@ -51,6 +51,9 @@
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/vedirect">{{ $t('menu.VedirectSettings') }}</router-link>
</li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{ $t('menu.DeviceManager') }}</router-link>
</li>
<li>
<hr class="dropdown-divider" />
</li>

View File

@ -0,0 +1,103 @@
<template>
<CardElement :text="$t('pininfo.PinOverview')" textVariant="text-bg-primary">
<div class="table-responsive">
<table class="table table-hover table-condensed">
<thead>
<tr>
<th>{{ $t('pininfo.Category') }}</th>
<th>{{ $t('pininfo.Name') }}</th>
<th>{{ $t('pininfo.ValueSelected') }}</th>
<th>{{ $t('pininfo.ValueActive') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="6">NRF24</td>
<td>MISO</td>
<td>{{ selectedPinAssignment?.nrf24?.miso }}</td>
<td>{{ currentPinAssignment?.nrf24?.miso }}</td>
</tr>
<tr>
<td>MOSI</td>
<td>{{ selectedPinAssignment?.nrf24?.mosi }}</td>
<td>{{ currentPinAssignment?.nrf24?.mosi }}</td>
</tr>
<tr>
<td>CLK</td>
<td>{{ selectedPinAssignment?.nrf24?.clk }}</td>
<td>{{ currentPinAssignment?.nrf24?.clk }}</td>
</tr>
<tr>
<td>IRQ</td>
<td>{{ selectedPinAssignment?.nrf24?.irq }}</td>
<td>{{ currentPinAssignment?.nrf24?.irq }}</td>
</tr>
<tr>
<td>EN</td>
<td>{{ selectedPinAssignment?.nrf24?.en }}</td>
<td>{{ currentPinAssignment?.nrf24?.en }}</td>
</tr>
<tr>
<td>CS</td>
<td>{{ selectedPinAssignment?.nrf24?.cs }}</td>
<td>{{ currentPinAssignment?.nrf24?.cs }}</td>
</tr>
<tr>
<td rowspan="7">Ethernet</td>
<td>enabled</td>
<td>{{ selectedPinAssignment?.eth?.enabled }}</td>
<td>{{ currentPinAssignment?.eth?.enabled }}</td>
</tr>
<tr>
<td>phy_addr</td>
<td>{{ selectedPinAssignment?.eth?.phy_addr }}</td>
<td>{{ currentPinAssignment?.eth?.phy_addr }}</td>
</tr>
<tr>
<td>power</td>
<td>{{ selectedPinAssignment?.eth?.power }}</td>
<td>{{ currentPinAssignment?.eth?.power }}</td>
</tr>
<tr>
<td>mdc</td>
<td>{{ selectedPinAssignment?.eth?.mdc }}</td>
<td>{{ currentPinAssignment?.eth?.mdc }}</td>
</tr>
<tr>
<td>mdio</td>
<td>{{ selectedPinAssignment?.eth?.mdio }}</td>
<td>{{ currentPinAssignment?.eth?.mdio }}</td>
</tr>
<tr>
<td>type</td>
<td>{{ selectedPinAssignment?.eth?.type }}</td>
<td>{{ currentPinAssignment?.eth?.type }}</td>
</tr>
<tr>
<td>clk_mode</td>
<td>{{ selectedPinAssignment?.eth?.clk_mode }}</td>
<td>{{ currentPinAssignment?.eth?.clk_mode }}</td>
</tr>
</tbody>
</table>
</div>
</CardElement>
</template>
<script lang="ts">
import CardElement from '@/components/CardElement.vue';
import type { Device } from '@/types/PinMapping';
import { defineComponent, type PropType } from 'vue';
export default defineComponent({
components: {
CardElement,
},
props: {
selectedPinAssignment: { type: Object as PropType<Device | undefined>, required: true },
currentPinAssignment: { type: Object as PropType<Device | undefined>, required: true },
},
});
</script>

View File

@ -19,7 +19,7 @@
</tr>
<tr>
<th>{{ $t('wifiapinfo.Stations') }}</th>
<td>{{ networkStatus.ap_stationnum }}</td>
<td>{{ $n(networkStatus.ap_stationnum, 'decimal') }}</td>
</tr>
</tbody>
</table>

View File

@ -19,11 +19,11 @@
</tr>
<tr>
<th>{{ $t('wifistationinfo.Quality') }}</th>
<td>{{ getRSSIasQuality(networkStatus.sta_rssi) }} %</td>
<td>{{ $n(getRSSIasQuality(networkStatus.sta_rssi), 'percent') }}</td>
</tr>
<tr>
<th>{{ $t('wifistationinfo.Rssi') }}</th>
<td>{{ networkStatus.sta_rssi }}</td>
<td>{{ $n(networkStatus.sta_rssi, 'decimal') }}</td>
</tr>
</tbody>
</table>
@ -55,7 +55,7 @@ export default defineComponent({
quality = 2 * (rssi + 100);
}
return quality;
return quality / 100;
},
},
});

View File

@ -8,6 +8,7 @@
"InverterSettings": "Wechselrichter Einstellungen",
"SecuritySettings": "Sicherheitseinstellungen",
"DTUSettings": "DTU Einstellungen",
"DeviceManager": "Geräte-Manager",
"VedirectSettings": "Ve.direct Settings",
"ConfigManagement": "Konfigurationsverwaltung",
"FirmwareUpgrade": "Firmware Aktualisierung",
@ -84,7 +85,8 @@
"10001": "Das Passwort muss zwischen 8 und {max} Zeichen lang sein!",
"10002": "Authentifizierung erfolgreich!",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004"
"11002": "@:apiresponse:5004",
"12001": "Profil muss zwischen 1 und {max} Zeichen lang sein!"
},
"home": {
"LiveData": "Live Daten",
@ -461,6 +463,8 @@
"BackupHeader": "Sicherung: Sicherung der Konfigurationsdatei",
"BackupConfig": "Sicherung der Konfigurationsdatei",
"Backup": "Sichern",
"Restore": "Wiederherstellen",
"NoFileSelected": "Keine Datei Ausgewählt",
"RestoreHeader": "Wiederherstellen: Wiederherstellen der Konfigurationsdatei",
"Back": "Zurück",
"UploadSuccess": "Erfolgreich hochgeladen",
@ -513,5 +517,20 @@
"TimeSyncLink": "Bitte überprüfen Sie Ihre Zeiteinstellungen.",
"DefaultPassword": "Sie verwenden das Standardpasswort für die Weboberfläche und den Notfall Access Point. Dies ist potenziell unsicher.",
"DefaultPasswordLink": "Bitte ändern Sie das Passwort."
},
"deviceadmin": {
"DeviceManager": "Geräte-Manager",
"PinAssignment": "Anschlusseinstellungen",
"SelectedProfile": "Ausgewähltes Profil:",
"DefaultProfile": "(Standard Einstellungen)",
"ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.",
"Save": "@:dtuadmin.Save"
},
"pininfo": {
"PinOverview": "Anschlussübersicht",
"Category": "Kategorie",
"Name": "Name",
"ValueSelected": "Ausgewählt",
"ValueActive": "Aktiv"
}
}

View File

@ -8,6 +8,7 @@
"InverterSettings": "Inverter Settings",
"SecuritySettings": "Security Settings",
"DTUSettings": "DTU Settings",
"DeviceManager": "Device-Manager",
"VedirectSettings": "Ve.direct Settings",
"ConfigManagement": "Config Management",
"FirmwareUpgrade": "Firmware Upgrade",
@ -84,7 +85,8 @@
"10001": "Password must between 8 and {max} characters long!",
"10002": "Authentication successfull!",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004"
"11002": "@:apiresponse:5004",
"12001": "Profil must between 1 and {max} characters long!"
},
"home": {
"LiveData": "Live Data",
@ -461,6 +463,8 @@
"BackupHeader": "Backup: Configuration File Backup",
"BackupConfig": "Backup the configuration file",
"Backup": "Backup",
"Restore": "Restore",
"NoFileSelected": "No file selected",
"RestoreHeader": "Restore: Restore the Configuration File",
"Back": "Back",
"UploadSuccess": "Upload Success",
@ -513,5 +517,21 @@
"TimeSyncLink": "Please check your time settings.",
"DefaultPassword": "You are using the default password for the web interface and the emergency access point. This is potentially insecure.",
"DefaultPasswordLink": "Please change the password."
},
"deviceadmin": {
"DeviceManager": "Device-Manager",
"PinAssignment": "Connection settings",
"SelectedProfile": "Selected profile:",
"DefaultProfile": "(Default settings)",
"ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.",
"Save": "@:dtuadmin.Save"
},
"pininfo": {
"PinOverview": "Connection overview",
"Category": "Category",
"Name": "Name",
"Number": "Number",
"ValueSelected": "Selected",
"ValueActive": "Active"
}
}

View File

@ -8,6 +8,7 @@
"InverterSettings": "Paramètres des onduleurs",
"SecuritySettings": "Paramètres de sécurité",
"DTUSettings": "Paramètres DTU",
"DeviceManager": "Device-Manager",
"VedirectSettings": "Paramètres Ve.direct",
"ConfigManagement": "Gestion de la configuration",
"FirmwareUpgrade": "Mise à jour du firmware",
@ -84,7 +85,8 @@
"10001": "Le mot de passe doit comporter entre 8 et {max} caractères !",
"10002": "Authentification réussie !",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004"
"11002": "@:apiresponse:5004",
"12001": "Profil must between 1 and {max} characters long!"
},
"home": {
"LiveData": "Données en direct",
@ -307,9 +309,7 @@
"invertertotalinfo": {
"TotalYieldTotal": "Rendement total",
"TotalYieldDay": "Rendement du jour",
"TotalPower": "Puissance de l'installation",
"TotalVoltage": "Tension moyenne",
"TotalCurrent": "Courant"
"TotalPower": "Puissance de l'installation"
},
"inverterchannelproperty": {
"Power": "Puissance",
@ -449,6 +449,7 @@
"InverterName": "Nom de l'onduleur :",
"InverterNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour votre onduleur.",
"StringName": "Nom de la ligne {num}:",
"StringNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour le port respectif de votre onduleur.",
"StringMaxPower": "Puissance maximale de la ligne {num}:",
"StringMaxPowerHint": "Entrez la puissance maximale des panneaux solaires connectés.",
"InverterHint": "*) Entrez le W<sub>p</sub> du canal pour calculer l'irradiation.",
@ -462,6 +463,8 @@
"BackupHeader": "Sauvegarder le fichier de configuration",
"BackupConfig": "Fichier de configuration",
"Backup": "Sauvegarder",
"Restore": "Restore",
"NoFileSelected": "No file selected",
"RestoreHeader": "Restaurer le fichier de configuration",
"Back": "Retour",
"UploadSuccess": "Succès du téléversement",
@ -514,5 +517,20 @@
"TimeSyncLink": "Veuillez vérifier vos paramètres horaires.",
"DefaultPassword": "Vous utilisez le mot de passe par défaut pour l'interface Web et le point d'accès d'urgence. Ceci est potentiellement non sécurisé.",
"DefaultPasswordLink": "Merci de changer le mot de passe."
},
"deviceadmin": {
"DeviceManager": "Device-Manager",
"PinAssignment": "Connection settings",
"SelectedProfile": "Selected profile:",
"DefaultProfile": "(Default settings)",
"ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.",
"Save": "@:dtuadmin.Save"
},
"pininfo": {
"PinOverview": "Connection overview",
"Category": "Category",
"Name": "Name",
"ValueSelected": "Selected",
"ValueActive": "Active"
}
}

View File

@ -57,4 +57,58 @@ export const dateTimeFormats: I18nOptions["datetimeFormats"] = {
}
};
export const numberFormats: I18nOptions["numberFormats"] = {
[Locales.EN]: {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0
},
decimalTwoDigits: {
style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2
},
percent: {
style: 'percent',
},
kilobyte: {
style: 'unit', unit: 'kilobyte',
},
},
[Locales.DE]: {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0
},
decimalTwoDigits: {
style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2
},
percent: {
style: 'percent',
},
kilobyte: {
style: 'unit', unit: 'kilobyte',
},
},
[Locales.FR]: {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0
},
decimalTwoDigits: {
style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2
},
percent: {
style: 'percent',
},
kilobyte: {
style: 'unit', unit: 'kilobyte',
},
},
};
export const defaultLocale = Locales.EN;

View File

@ -2,7 +2,7 @@ import mitt from 'mitt'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import App from './App.vue'
import { defaultLocale, messages, dateTimeFormats } from './locales'
import { defaultLocale, messages, dateTimeFormats, numberFormats } from './locales'
import { tooltip } from './plugins/bootstrap'
import router from './router'
@ -23,6 +23,7 @@ const i18n = createI18n({
fallbackLocale: defaultLocale,
messages,
datetimeFormats: dateTimeFormats,
numberFormats: numberFormats
})
app.use(router)

View File

@ -1,6 +1,7 @@
import AboutView from '@/views/AboutView.vue';
import ConfigAdminView from '@/views/ConfigAdminView.vue';
import ConsoleInfoView from '@/views/ConsoleInfoView.vue';
import DeviceAdminView from '@/views/DeviceAdminView.vue'
import DtuAdminView from '@/views/DtuAdminView.vue';
import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue';
import HomeView from '@/views/HomeView.vue';
@ -98,6 +99,11 @@ const router = createRouter({
name: 'DTU Settings',
component: DtuAdminView
},
{
path: '/settings/device',
name: 'Device Manager',
component: DeviceAdminView
},
{
path: '/firmware/upgrade',
name: 'Firmware Upgrade',

View File

@ -0,0 +1,7 @@
export interface ConfigFileInfo {
name: string;
}
export interface ConfigFileList {
configs: Array<ConfigFileInfo>;
}

View File

@ -0,0 +1,5 @@
import type { Device } from "./PinMapping";
export interface DeviceConfig {
curPin: Device;
}

View File

@ -0,0 +1,26 @@
export interface Nrf24 {
miso: number;
mosi: number;
clk: number;
irq: number;
en: number;
cs: number;
}
export interface Ethernet {
enabled: boolean;
phy_addr: number;
power: number;
mdc: number;
mdio: number;
type: number;
clk_mode: number;
}
export interface Device {
name: string;
nrf24: Nrf24;
eth: Ethernet;
}
export interface PinMapping extends Array<Device>{}

View File

@ -1,10 +1,8 @@
import { isLoggedIn, login, logout } from './authentication';
import { formatNumber } from './number';
import { timestampToString } from './time';
export {
timestampToString,
formatNumber,
login,
logout,
isLoggedIn,
@ -12,7 +10,6 @@ export {
export default {
timestampToString,
formatNumber,
login,
logout,
isLoggedIn,

View File

@ -1,5 +0,0 @@
export const formatNumber = (num: number, digits: number): string => {
return new Intl.NumberFormat(
undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits }
).format(num);
}

View File

@ -5,9 +5,22 @@
</BootstrapAlert>
<CardElement :text="$t('configadmin.BackupHeader')" textVariant="text-bg-primary" center-content>
{{ $t('configadmin.BackupConfig') }}
<button class="btn btn-primary" @click="downloadConfig">{{ $t('configadmin.Backup') }}
</button>
<div class="row g-3 align-items-center">
<div class="col-sm">
{{ $t('configadmin.BackupConfig') }}
</div>
<div class="col-sm">
<select class="form-select" v-model="backupFileSelect">
<option v-for="(file) in fileList.configs" :key="file.name" :value="file.name">
{{ file.name }}
</option>
</select>
</div>
<div class="col-sm">
<button class="btn btn-primary" @click="downloadConfig">{{ $t('configadmin.Backup') }}
</button>
</div>
</div>
</CardElement>
<CardElement :text="$t('configadmin.RestoreHeader')" textVariant="text-bg-primary" center-content add-space>
@ -38,8 +51,20 @@
</div>
<div v-else-if="!uploading">
<div class="form-group pt-2 mt-3">
<input class="form-control" type="file" ref="file" accept=".json" @change="uploadConfig" />
<div class="row g-3 align-items-center form-group pt-2">
<div class="col-sm">
<select class="form-select" v-model="restoreFileSelect">
<option selected value="config.json">Main Config (config.json)</option>
<option selected value="pin_mapping.json">Pin Mapping (pin_mapping.json)</option>
</select>
</div>
<div class="col-sm">
<input class="form-control" type="file" ref="file" accept=".json" />
</div>
<div class="col-sm">
<button class="btn btn-primary" @click="uploadConfig">{{ $t('configadmin.Restore') }}
</button>
</div>
</div>
</div>
@ -89,7 +114,8 @@
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import { authHeader, handleResponse, isLoggedIn } from '@/utils/authentication';
import type { ConfigFileList } from '@/types/Config';
import { authHeader, handleResponse } from '@/utils/authentication';
import * as bootstrap from 'bootstrap';
import {
BIconArrowLeft,
@ -119,14 +145,16 @@ export default defineComponent({
UploadError: "",
UploadSuccess: false,
file: {} as Blob,
fileList: {} as ConfigFileList,
backupFileSelect: "",
restoreFileSelect: "config.json",
};
},
mounted() {
if (!isLoggedIn()) {
this.$router.push({ path: "/login", query: { returnUrl: this.$router.currentRoute.value.fullPath } });
}
this.modalFactoryReset = new bootstrap.Modal('#factoryReset');
this.loading = false;
},
created() {
this.getFileList();
},
methods: {
onFactoryResetModal() {
@ -154,27 +182,42 @@ export default defineComponent({
)
this.modalFactoryReset.hide();
},
getFileList() {
this.loading = true;
fetch("/api/config/list", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.fileList = data;
if (this.fileList.configs) {
this.backupFileSelect = this.fileList.configs[0].name;
}
this.loading = false;
});
},
downloadConfig() {
fetch("/api/config/get", { headers: authHeader() })
fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() })
.then(res => res.blob())
.then(blob => {
var file = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = file;
a.download = "config.json";
a.download = this.backupFileSelect;
document.body.appendChild(a);
a.click();
a.remove();
});
},
uploadConfig(event: Event | null) {
uploadConfig() {
this.uploading = true;
const formData = new FormData();
if (event !== null) {
const target = event.target as HTMLInputElement;
if (target.files !== null) {
this.file = target.files[0];
}
const target = this.$refs.file as HTMLInputElement; // event.target as HTMLInputElement;
if (target.files !== null && target.files?.length > 0) {
this.file = target.files[0];
} else {
this.UploadError = this.$t("configadmin.NoFileSelected");
this.uploading = false;
this.progress = 0;
return;
}
const request = new XMLHttpRequest();
request.addEventListener("load", () => {
@ -196,7 +239,7 @@ export default defineComponent({
request.withCredentials = true;
formData.append("config", this.file, "config");
request.open("post", "/api/config/upload");
request.open("post", "/api/config/upload?file=" + this.restoreFileSelect);
authHeader().forEach((value, key) => {
request.setRequestHeader(key, value);
});
@ -205,6 +248,7 @@ export default defineComponent({
clear() {
this.UploadError = "";
this.UploadSuccess = false;
this.getFileList();
},
},
});

View File

@ -0,0 +1,138 @@
<template>
<BasePage :title="$t('deviceadmin.DeviceManager')" :isLoading="dataLoading || pinMappingLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-pin-tab" data-bs-toggle="tab" data-bs-target="#nav-pin"
type="button" role="tab" aria-controls="nav-pin" aria-selected="true">{{
$t('deviceadmin.PinAssignment')
}}</button>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-pin" role="tabpanel" aria-labelledby="nav-pin-tab"
tabindex="0">
<div class="card">
<div class="card-body">
<form @submit="savePinConfig">
<div class="row mb-3">
<label for="inputPinProfile" class="col-sm-2 col-form-label">{{
$t('deviceadmin.SelectedProfile')
}}</label>
<div class="col-sm-10">
<select class="form-select" id="inputPinProfile"
v-model="deviceConfigList.curPin.name">
<option v-for="device in pinMappingList" :value="device.name" :key="device.name">
{{ device.name }}
</option>
</select>
</div>
</div>
<div class="alert alert-danger mt-3" role="alert" v-html="$t('deviceadmin.ProfileHint')">
</div>
<PinInfo
:selectedPinAssignment="pinMappingList.find(i => i.name === deviceConfigList.curPin.name)"
:currentPinAssignment="deviceConfigList.curPin" />
<button type="submit" class="btn btn-primary mb-3">{{ $t('deviceadmin.Save') }}</button>
</form>
</div>
</div>
</div>
</div>
</BasePage>
</template>
<script lang="ts">
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import PinInfo from '@/components/PinInfo.vue';
import type { DeviceConfig } from "@/types/DeviceConfig";
import type { PinMapping, Device } from "@/types/PinMapping";
import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
PinInfo,
},
data() {
return {
dataLoading: true,
pinMappingLoading: true,
deviceConfigList: {} as DeviceConfig,
pinMappingList: {} as PinMapping,
alertMessage: "",
alertType: "info",
showAlert: false,
}
},
created() {
this.getDeviceConfig();
this.getPinMappingList();
},
methods: {
getPinMappingList() {
this.pinMappingLoading = true;
fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(data) => {
this.pinMappingList = data;
}
)
.catch(() => {
this.pinMappingList = Array<Device>();
})
.finally(() => {
this.pinMappingList.push({
"name": this.$t('deviceadmin.DefaultProfile')
} as Device);
this.pinMappingList.sort((a, b) => (a.name < b.name) ? -1 : 1);
this.pinMappingLoading = false;
});
},
getDeviceConfig() {
this.dataLoading = true;
fetch("/api/device/config", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(data) => {
this.deviceConfigList = data;
this.dataLoading = false;
}
);
},
savePinConfig(e: Event) {
e.preventDefault();
const formData = new FormData();
formData.append("data", JSON.stringify(this.deviceConfigList));
fetch("/api/device/config", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
this.alertType = response.type;
this.showAlert = true;
}
);
},
},
});
</script>

View File

@ -41,11 +41,11 @@
</div>
<div style="padding-right: 2em;">
{{ $t('home.CurrentLimit') }}<template v-if="inverter.limit_absolute > -1"> {{
formatNumber(inverter.limit_absolute, 0)
}} W | </template>{{ formatNumber(inverter.limit_relative, 0) }} %
$n(inverter.limit_absolute, 'decimalNoDigits')
}} W | </template>{{ $n(inverter.limit_relative / 100, 'percent') }}
</div>
<div style="padding-right: 2em;">
{{ $t('home.DataAge') }} {{ $t('home.Seconds', {'val': inverter.data_age }) }}
{{ $t('home.DataAge') }} {{ $t('home.Seconds', {'val': $n(inverter.data_age) }) }}
<template v-if="inverter.data_age > 300">
/ {{ calculateAbsoluteTime(inverter.data_age) }}
</template>
@ -323,7 +323,6 @@ import type { EventlogItems } from '@/types/EventlogStatus';
import type { LimitConfig } from '@/types/LimitConfig';
import type { LimitStatus } from '@/types/LimitStatus';
import type { Inverter, LiveData } from '@/types/LiveDataStatus';
import { formatNumber } from '@/utils';
import { authHeader, authUrl, handleResponse, isLoggedIn } from '@/utils/authentication';
import * as bootstrap from 'bootstrap';
import {
@ -443,19 +442,20 @@ export default defineComponent({
computed: {
currentLimitAbsolute(): string {
if (this.currentLimitList.max_power > 0) {
return formatNumber(this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100, 2);
return this.$n(this.currentLimitList.limit_relative * this.currentLimitList.max_power / 100,
'decimalTwoDigits');
}
return "0";
},
currentLimitRelative(): string {
return formatNumber(this.currentLimitList.limit_relative, 2);
return this.$n(this.currentLimitList.limit_relative,
'decimalTwoDigits');
},
inverterData(): Inverter[] {
return this.liveData.inverters;
}
},
methods: {
formatNumber,
isLoggedIn,
getInitialData() {
this.dataLoading = true;

View File

@ -59,7 +59,7 @@
<InputElement v-show="mqttConfigList.mqtt_tls"
:label="$t('mqttadmin.RootCa')"
v-model="mqttConfigList.mqtt_root_ca_cert"
type="textarea" maxlength="2048" rows="10"/>
type="textarea" maxlength="2560" rows="10"/>
</CardElement>
<CardElement :text="$t('mqttadmin.LwtParameters')" textVariant="text-bg-primary" add-space

View File

@ -366,35 +366,35 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz#93815beffd23db46288c787352a8ea31a0c03e5e"
integrity sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==
"@volar/language-core@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.0.19.tgz#787de46fd7de64c50c8500d799905e9da8d1ce36"
integrity sha512-BRxhwqn66VHeLIxxgV4ybY9NDtwMp2bl1w7085qlK7i1pa4jeFR5lJG2U5qd0oI3e0PIWML+PryxSrKNd3+SZw==
"@volar/language-core@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.0.24.tgz#5d767571e77728464635e61af1debca944811fe0"
integrity sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg==
dependencies:
"@volar/source-map" "1.0.19"
"@volar/source-map" "1.0.24"
muggle-string "^0.1.0"
"@volar/source-map@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.0.19.tgz#6939a2e47ff99166af653ab92fb8dfafba170279"
integrity sha512-5fYKsl1evR/QAZ9LADto3kzbYKfpjZLWS9reNpxGR3ODPFTpaJgYk4lqghFyq4yU7/e/ZPZ1zLXjEsnL526URw==
"@volar/source-map@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.0.24.tgz#ad4c827fea5c26b4bf38a86d983e7deb65b1c61e"
integrity sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A==
dependencies:
muggle-string "^0.1.0"
"@volar/typescript@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.0.19.tgz#e2dd5b9868c6233df5dafcf514d76f095c6c0233"
integrity sha512-S6n945uhpc5J1qCVXVV4tz4k1nyxWaoG+wqy9TYdRDazPHeq9l45WDg58g/ehblUWux85TZN8i3zdsLRLkFrdw==
"@volar/typescript@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.0.24.tgz#f934eda9774b31abdff53efc56782cd2623723d5"
integrity sha512-f8hCSk+PfKR1/RQHxZ79V1NpDImHoivqoizK+mstphm25tn/YJ/JnKNjZHB+o21fuW0yKlI26NV3jkVb2Cc/7A==
dependencies:
"@volar/language-core" "1.0.19"
"@volar/language-core" "1.0.24"
"@volar/vue-language-core@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@volar/vue-language-core/-/vue-language-core-1.0.19.tgz#351f3cf08c1039259d422eabaa49d22fcc5bbaa3"
integrity sha512-3mIjJvQ+0tNOp+U9+Nggy92HYIqnltf882UMG9RuNHrd0Jn/rdvjRBs0jNTzwYDV9tn3tjDHGIfQak9XrUCaRg==
"@volar/vue-language-core@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@volar/vue-language-core/-/vue-language-core-1.0.24.tgz#81d180a8e09a53cb575e83acb79a31493891a1a4"
integrity sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg==
dependencies:
"@volar/language-core" "1.0.19"
"@volar/source-map" "1.0.19"
"@volar/language-core" "1.0.24"
"@volar/source-map" "1.0.24"
"@vue/compiler-dom" "^3.2.45"
"@vue/compiler-sfc" "^3.2.45"
"@vue/reactivity" "^3.2.45"
@ -402,13 +402,13 @@
minimatch "^5.1.1"
vue-template-compiler "^2.7.14"
"@volar/vue-typescript@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@volar/vue-typescript/-/vue-typescript-1.0.19.tgz#bd1dc58bf9aecb760a9052fbc34f9e6e6ad6a406"
integrity sha512-HKaLCz/lb5xkJ1SyaMmms0Ww/OVStQ16qWttSbHRnnyRV/IDMFrwlovA/bIAPzHUq8EVoDAznRVsCysr2QCOGA==
"@volar/vue-typescript@1.0.24":
version "1.0.24"
resolved "https://registry.yarnpkg.com/@volar/vue-typescript/-/vue-typescript-1.0.24.tgz#bef9b2bfb1b108c0f6cb12ec6fbf449b43fc8257"
integrity sha512-9a25oHDvGaNC0okRS47uqJI6FxY4hUQZUsxeOUFHcqVxZEv8s17LPuP/pMMXyz7jPygrZubB/qXqHY5jEu/akA==
dependencies:
"@volar/typescript" "1.0.19"
"@volar/vue-language-core" "1.0.19"
"@volar/typescript" "1.0.24"
"@volar/vue-language-core" "1.0.24"
"@vue/compiler-core@3.2.45":
version "3.2.45"
@ -850,10 +850,10 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-plugin-vue@^9.8.0:
version "9.8.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.8.0.tgz#91de2aabbee8cdbef078ccd4f650a9ecfa445f4f"
integrity sha512-E/AXwcTzunyzM83C2QqDHxepMzvI2y6x+mmeYHbVDQlKFqmKYvRrhaVixEeeG27uI44p9oKDFiyCRw4XxgtfHA==
eslint-plugin-vue@^9.9.0:
version "9.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.9.0.tgz#ac788ebccd2eb94d846a507df55da50693b80c91"
integrity sha512-YbubS7eK0J7DCf0U2LxvVP7LMfs6rC6UltihIgval3azO3gyDwEGVgsCMe1TmDiEkl6GdMKfRpaME6QxIYtzDQ==
dependencies:
eslint-utils "^3.0.0"
natural-compare "^1.4.0"
@ -896,10 +896,10 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@^8.31.0:
version "8.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.31.0.tgz#75028e77cbcff102a9feae1d718135931532d524"
integrity sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==
eslint@^8.32.0:
version "8.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.32.0.tgz#d9690056bb6f1a302bd991e7090f5b68fbaea861"
integrity sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==
dependencies:
"@eslint/eslintrc" "^1.4.1"
"@humanwhocodes/config-array" "^0.11.8"
@ -2081,15 +2081,15 @@ vite-plugin-compression@^0.5.1:
debug "^4.3.3"
fs-extra "^10.0.0"
vite-plugin-css-injected-by-js@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.2.0.tgz#5ff95fbee027698e399a30437335214dc09bb989"
integrity sha512-SRGuyY1WUHj7cPzv7AIE0bG5Cb+vioxuq3CkFc1j0b8z5Cy3rXLG8SwxjriylFcZAY7tH2jU4i1bsCJRE/ou6g==
vite-plugin-css-injected-by-js@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.4.0.tgz#32eb77e3ea0c45fbecf1c5f1e65703cccd42ecdb"
integrity sha512-fQkJ5baPEasjjJLxHINLjXuPREO61VIDFUeUqleEBghOLfZZe/k/zrxG5b3kFZXu6JtdI11pnwtj3dh3CN9X4Q==
vite@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.3.tgz#de27ad3f263a03ae9419cdc8bc07721eadcba8b9"
integrity sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==
vite@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.0.4.tgz#4612ce0b47bbb233a887a54a4ae0c6e240a0da31"
integrity sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==
dependencies:
esbuild "^0.16.3"
postcss "^8.4.20"
@ -2136,13 +2136,13 @@ vue-template-compiler@^2.7.14:
de-indent "^1.0.2"
he "^1.2.0"
vue-tsc@^1.0.19:
version "1.0.19"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.0.19.tgz#dffbcb8abb6675626719bba438caf954d7615e58"
integrity sha512-UuI4G9PwV07Q2U+xYDLP5y3aUXTfuIF0Exy0qXT8+BbLlahubQ2r2PGSodSBnHxAhm/XsrD0KleC2rSzLKXDfQ==
vue-tsc@^1.0.24:
version "1.0.24"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.0.24.tgz#c0b270a7c8422408d3b6694fee61b39a4b9e4740"
integrity sha512-mmU1s5SAqE1nByQAiQnao9oU4vX+mSdsgI8H57SfKH6UVzq/jP9+Dbi2GaV+0b4Cn361d2ln8m6xeU60ApiEXg==
dependencies:
"@volar/vue-language-core" "1.0.19"
"@volar/vue-typescript" "1.0.19"
"@volar/vue-language-core" "1.0.24"
"@volar/vue-typescript" "1.0.24"
vue@^3.2.45:
version "3.2.45"

Binary file not shown.