Merge remote-tracking branch 'tbnobody/OpenDTU/master'
This commit is contained in:
commit
1fc0e76c41
@ -10,6 +10,10 @@ It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with th
|
||||
## Screenshots
|
||||
Several screenshots of the frontend can be found here: [Screenshots](docs/screenshots/README.md)
|
||||
|
||||
## Builds
|
||||
Different builds from existing installations can be found here [Builds](docs/builds/README.md)
|
||||
Like to show your own build? Just send me a Pull Request.
|
||||
|
||||
I extended the original OpenDTU software to support also Victron's Ve.Direct protocol on the same chip. Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
|
||||
|
||||
Web-Live-Interface:
|
||||
|
||||
@ -4,3 +4,4 @@ More detailed descriptions for some topics can be found here.
|
||||
|
||||
## [MQTT Topic Documentation](MQTT_Topics.md)
|
||||
## [Web API Documentation](Web-API.md)
|
||||
## [Builds](builds/README.md)
|
||||
BIN
docs/builds/202654506-8a4ac4ef-c883-490e-8ee1-1e1f7fa34972.jpg
Normal file
BIN
docs/builds/202654506-8a4ac4ef-c883-490e-8ee1-1e1f7fa34972.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
19
docs/builds/README.md
Normal file
19
docs/builds/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Builds using different boards
|
||||
|
||||
## ESP32 Dev Board
|
||||
### Build by @Marc--
|
||||
* Used build environment: generic
|
||||
* Case: https://www.thingiverse.com/thing:5435911
|
||||

|
||||
|
||||
### Build by @cepresso
|
||||
* Used build environment: generic
|
||||
* Case: https://www.printables.com/de/model/293003-sol-opendtu-esp32-nrf24l01-case
|
||||

|
||||
|
||||
## LILYGO® TTGO T-Internet-POE
|
||||
### Build by @fromCologne
|
||||
* Used build environment: LilyGO_T_ETH_POE
|
||||
* Board info: http://www.lilygo.cn/claprod_view.aspx?TypeId=21&Id=1344&FId=t28:21:28
|
||||
* Case: https://www.thingiverse.com/thing:5244895
|
||||

|
||||
BIN
docs/builds/large_display_PXL_20220715_145622277.jpg
Normal file
BIN
docs/builds/large_display_PXL_20220715_145622277.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
docs/builds/sol.webp
Normal file
BIN
docs/builds/sol.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@ -3,9 +3,8 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#define CONFIG_FILENAME "/config.bin"
|
||||
#define CONFIG_FILENAME_JSON "/config.json"
|
||||
#define CONFIG_VERSION 0x00011600 // 0.1.22 // make sure to clean all after change
|
||||
#define CONFIG_FILENAME "/config.json"
|
||||
#define CONFIG_VERSION 0x00011700 // 0.1.23 // make sure to clean all after change
|
||||
|
||||
#define WIFI_MAX_SSID_STRLEN 31
|
||||
#define WIFI_MAX_PASSWORD_STRLEN 64
|
||||
@ -15,10 +14,9 @@
|
||||
#define NTP_MAX_TIMEZONE_STRLEN 50
|
||||
#define NTP_MAX_TIMEZONEDESCR_STRLEN 50
|
||||
|
||||
#define MQTT_MAX_HOSTNAME_OLD_STRLEN 31
|
||||
#define MQTT_MAX_HOSTNAME_STRLEN 128
|
||||
#define MQTT_MAX_USERNAME_STRLEN 32
|
||||
#define MQTT_MAX_PASSWORD_STRLEN 32
|
||||
#define MQTT_MAX_USERNAME_STRLEN 64
|
||||
#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
|
||||
@ -27,12 +25,19 @@
|
||||
#define INV_MAX_COUNT 10
|
||||
#define INV_MAX_CHAN_COUNT 4
|
||||
|
||||
#define CHAN_MAX_NAME_STRLEN 31
|
||||
|
||||
#define JSON_BUFFER_SIZE 6144
|
||||
|
||||
struct CHANNEL_CONFIG_T {
|
||||
uint16_t MaxChannelPower;
|
||||
char Name[CHAN_MAX_NAME_STRLEN];
|
||||
};
|
||||
|
||||
struct INVERTER_CONFIG_T {
|
||||
uint64_t Serial;
|
||||
char Name[INV_MAX_NAME_STRLEN + 1];
|
||||
uint16_t MaxChannelPower[INV_MAX_CHAN_COUNT];
|
||||
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
|
||||
};
|
||||
|
||||
struct CONFIG_T {
|
||||
@ -54,7 +59,6 @@ struct CONFIG_T {
|
||||
char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1];
|
||||
|
||||
bool Mqtt_Enabled;
|
||||
char Mqtt_Hostname_Short[MQTT_MAX_HOSTNAME_OLD_STRLEN + 1]; // Deprecated but for config compatibility
|
||||
uint Mqtt_Port;
|
||||
char Mqtt_Username[MQTT_MAX_USERNAME_STRLEN + 1];
|
||||
char Mqtt_Password[MQTT_MAX_PASSWORD_STRLEN + 1];
|
||||
@ -98,9 +102,7 @@ public:
|
||||
CONFIG_T& get();
|
||||
|
||||
INVERTER_CONFIG_T* getFreeInverterSlot();
|
||||
|
||||
private:
|
||||
bool readJson();
|
||||
INVERTER_CONFIG_T* getInverterConfig(uint64_t serial);
|
||||
};
|
||||
|
||||
extern ConfigurationClass Configuration;
|
||||
@ -15,6 +15,7 @@ const devInfo_t devInfo[] = {
|
||||
{ { 0x10, 0x10, 0x40, ALL }, 400, "HM-400" },
|
||||
{ { 0x10, 0x11, 0x10, ALL }, 600, "HM-600" },
|
||||
{ { 0x10, 0x11, 0x20, ALL }, 700, "HM-700" },
|
||||
{ { 0x10, 0x11, 0x30, ALL }, 800, "HM-800" },
|
||||
{ { 0x10, 0x11, 0x40, ALL }, 800, "HM-800" },
|
||||
{ { 0x10, 0x12, 0x10, ALL }, 1200, "HM-1200" },
|
||||
{ { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500" },
|
||||
|
||||
@ -36,8 +36,8 @@ monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
|
||||
; Specify port here. Comment out (add ; in front of line) to use auto detection.
|
||||
monitor_port = COM4
|
||||
upload_port = COM4
|
||||
monitor_port = COM5
|
||||
upload_port = COM5
|
||||
|
||||
|
||||
[env:generic]
|
||||
@ -96,3 +96,21 @@ build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_CE=14
|
||||
-DHOYMILES_PIN_CS=15
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
[env:LilyGO_T_ETH_POE]
|
||||
; http://www.lilygo.cn/claprod_view.aspx?TypeId=21&Id=1344&FId=t28:21:28
|
||||
board = esp32dev
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=2
|
||||
-DHOYMILES_PIN_MOSI=15
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=34
|
||||
-DHOYMILES_PIN_CE=12
|
||||
-DHOYMILES_PIN_CS=4
|
||||
-DOPENDTU_ETHERNET
|
||||
-DETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT
|
||||
-DETH_POWER_PIN=-1
|
||||
-DETH_TYPE=ETH_PHY_LAN8720
|
||||
-DETH_ADDR=0
|
||||
-DETH_MDC_PIN=23
|
||||
-DETH_MDIO_PIN=18
|
||||
@ -12,63 +12,11 @@ CONFIG_T config;
|
||||
void ConfigurationClass::init()
|
||||
{
|
||||
memset(&config, 0x0, sizeof(config));
|
||||
config.Cfg_SaveCount = 0;
|
||||
config.Cfg_Version = CONFIG_VERSION;
|
||||
|
||||
// WiFi Settings
|
||||
strlcpy(config.WiFi_Ssid, WIFI_SSID, sizeof(config.WiFi_Ssid));
|
||||
strlcpy(config.WiFi_Password, WIFI_PASSWORD, sizeof(config.WiFi_Password));
|
||||
config.WiFi_Dhcp = WIFI_DHCP;
|
||||
strlcpy(config.WiFi_Hostname, APP_HOSTNAME, sizeof(config.WiFi_Hostname));
|
||||
|
||||
// NTP Settings
|
||||
strlcpy(config.Ntp_Server, NTP_SERVER, sizeof(config.Ntp_Server));
|
||||
strlcpy(config.Ntp_Timezone, NTP_TIMEZONE, sizeof(config.Ntp_Timezone));
|
||||
strlcpy(config.Ntp_TimezoneDescr, NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr));
|
||||
|
||||
// MqTT Settings
|
||||
config.Mqtt_Enabled = MQTT_ENABLED;
|
||||
strlcpy(config.Mqtt_Hostname, MQTT_HOST, sizeof(config.Mqtt_Hostname));
|
||||
config.Mqtt_Port = MQTT_PORT;
|
||||
strlcpy(config.Mqtt_Username, MQTT_USER, sizeof(config.Mqtt_Username));
|
||||
strlcpy(config.Mqtt_Password, MQTT_PASSWORD, sizeof(config.Mqtt_Password));
|
||||
strlcpy(config.Mqtt_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Topic));
|
||||
config.Mqtt_Retain = MQTT_RETAIN;
|
||||
config.Mqtt_Tls = MQTT_TLS;
|
||||
strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
|
||||
strlcpy(config.Mqtt_LwtTopic, MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic));
|
||||
strlcpy(config.Mqtt_LwtValue_Online, MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online));
|
||||
strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline));
|
||||
config.Mqtt_PublishInterval = MQTT_PUBLISH_INTERVAL;
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
config.Inverter[i].Serial = 0;
|
||||
strlcpy(config.Inverter[i].Name, "", 0);
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
config.Inverter[0].MaxChannelPower[c] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
config.Dtu_Serial = DTU_SERIAL;
|
||||
config.Dtu_PollInterval = DTU_POLL_INTERVAL;
|
||||
config.Dtu_PaLevel = DTU_PA_LEVEL;
|
||||
|
||||
config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED;
|
||||
config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE;
|
||||
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
|
||||
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;
|
||||
}
|
||||
|
||||
bool ConfigurationClass::write()
|
||||
{
|
||||
File f = LittleFS.open(CONFIG_FILENAME_JSON, "w");
|
||||
File f = LittleFS.open(CONFIG_FILENAME, "w");
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
@ -136,9 +84,11 @@ bool ConfigurationClass::write()
|
||||
inv["serial"] = config.Inverter[i].Serial;
|
||||
inv["name"] = config.Inverter[i].Name;
|
||||
|
||||
JsonArray channels = inv.createNestedArray("channels");
|
||||
JsonArray channel = inv.createNestedArray("channel");
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
channels.add(config.Inverter[i].MaxChannelPower[c]);
|
||||
JsonObject chanData = channel.createNestedObject();
|
||||
chanData["name"] = config.Inverter[i].channel[c].Name;
|
||||
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,30 +109,7 @@ bool ConfigurationClass::write()
|
||||
|
||||
bool ConfigurationClass::read()
|
||||
{
|
||||
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;
|
||||
}
|
||||
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
|
||||
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
// Deserialize the JSON document
|
||||
@ -282,9 +209,10 @@ bool ConfigurationClass::readJson()
|
||||
config.Inverter[i].Serial = inv["serial"] | 0ULL;
|
||||
strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name));
|
||||
|
||||
JsonArray channels = inv["channels"];
|
||||
JsonArray channel = inv["channel"];
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
config.Inverter[i].MaxChannelPower[c] = channels[c];
|
||||
config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0;
|
||||
strlcpy(config.Inverter[i].channel[c].Name, channel[c]["name"] | "", sizeof(config.Inverter[i].channel[c].Name));
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,81 +227,35 @@ bool ConfigurationClass::readJson()
|
||||
|
||||
void ConfigurationClass::migrate()
|
||||
{
|
||||
if (config.Cfg_Version < 0x00010400) {
|
||||
strlcpy(config.Ntp_Server, NTP_SERVER, sizeof(config.Ntp_Server));
|
||||
strlcpy(config.Ntp_Timezone, NTP_TIMEZONE, sizeof(config.Ntp_Timezone));
|
||||
strlcpy(config.Ntp_TimezoneDescr, NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr));
|
||||
if (config.Cfg_Version < 0x00011700) {
|
||||
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
|
||||
if (!f) {
|
||||
Serial.println(F("Failed to open file, cancel migration"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00010500) {
|
||||
config.Mqtt_Enabled = MQTT_ENABLED;
|
||||
strlcpy(config.Mqtt_Hostname, MQTT_HOST, sizeof(config.Mqtt_Hostname));
|
||||
config.Mqtt_Port = MQTT_PORT;
|
||||
strlcpy(config.Mqtt_Username, MQTT_USER, sizeof(config.Mqtt_Username));
|
||||
strlcpy(config.Mqtt_Password, MQTT_PASSWORD, sizeof(config.Mqtt_Password));
|
||||
strlcpy(config.Mqtt_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Topic));
|
||||
DynamicJsonDocument doc(JSON_BUFFER_SIZE);
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, f);
|
||||
if (error) {
|
||||
Serial.println(F("Failed to read file, cancel migration"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00010600) {
|
||||
config.Mqtt_Retain = MQTT_RETAIN;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00010700) {
|
||||
strlcpy(config.Mqtt_LwtTopic, MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic));
|
||||
strlcpy(config.Mqtt_LwtValue_Online, MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online));
|
||||
strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline));
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00010800) {
|
||||
JsonArray inverters = doc["inverters"];
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
config.Inverter[i].Serial = 0;
|
||||
strlcpy(config.Inverter[i].Name, "", 0);
|
||||
JsonObject inv = inverters[i].as<JsonObject>();
|
||||
JsonArray channels = inv["channels"];
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
config.Inverter[i].channel[c].MaxChannelPower = channels[c];
|
||||
strlcpy(config.Inverter[i].channel[c].Name, "", sizeof(config.Inverter[i].channel[c].Name));
|
||||
}
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00010900) {
|
||||
config.Dtu_Serial = DTU_SERIAL;
|
||||
config.Dtu_PollInterval = DTU_POLL_INTERVAL;
|
||||
config.Dtu_PaLevel = DTU_PA_LEVEL;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011000) {
|
||||
config.Mqtt_PublishInterval = MQTT_PUBLISH_INTERVAL;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011100) {
|
||||
init(); // Config will be completly incompatible after this update
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011200) {
|
||||
config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED;
|
||||
config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN;
|
||||
strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic));
|
||||
config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011300) {
|
||||
config.Mqtt_Tls = MQTT_TLS;
|
||||
strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert));
|
||||
config.Vedirect_Enabled = VEDIRECT_ENABLED;
|
||||
config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY;
|
||||
config.Vedirect_PollInterval = VEDIRECT_POLL_INTERVAL;
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011400) {
|
||||
strlcpy(config.Mqtt_Hostname, config.Mqtt_Hostname_Short, sizeof(config.Mqtt_Hostname_Short));
|
||||
}
|
||||
|
||||
if (config.Cfg_Version < 0x00011500) {
|
||||
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();
|
||||
read();
|
||||
}
|
||||
|
||||
CONFIG_T& ConfigurationClass::get()
|
||||
@ -392,4 +274,15 @@ INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
|
||||
return NULL;
|
||||
}
|
||||
|
||||
INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(uint64_t serial)
|
||||
{
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial == serial) {
|
||||
return &config.Inverter[i];
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ConfigurationClass Configuration;
|
||||
@ -83,6 +83,12 @@ void MqttPublishingClass::loop()
|
||||
|
||||
// Loop all channels
|
||||
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
|
||||
if (c > 0) {
|
||||
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
|
||||
if (inv_cfg != nullptr) {
|
||||
MqttSettings.publish(inv->serialString() + "/" + String(c) + "/name", inv_cfg->channel[c - 1].Name);
|
||||
}
|
||||
}
|
||||
for (uint8_t f = 0; f < sizeof(_publishFields); f++) {
|
||||
publishField(inv, c, _publishFields[f]);
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true);
|
||||
request->send(LittleFS, CONFIG_FILENAME, String(), true);
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
@ -96,7 +96,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
LittleFS.remove(CONFIG_FILENAME_JSON);
|
||||
LittleFS.remove(CONFIG_FILENAME);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
@ -127,7 +127,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_JSON, "w");
|
||||
request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w");
|
||||
}
|
||||
|
||||
if (len) {
|
||||
|
||||
@ -62,8 +62,11 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
max_channels = inv->Statistics()->getChannelCount();
|
||||
}
|
||||
|
||||
JsonArray channel = obj.createNestedArray("channel");
|
||||
for (uint8_t c = 0; c < max_channels; c++) {
|
||||
obj[F("max_power")][c] = config.Inverter[i].MaxChannelPower[c];
|
||||
JsonObject chanData = channel.createNestedObject();
|
||||
chanData["name"] = config.Inverter[i].channel[c].Name;
|
||||
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,7 +157,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
|
||||
if (inv != nullptr) {
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
inv->Statistics()->setChannelMaxPower(c, inverter->MaxChannelPower[c]);
|
||||
inv->Statistics()->setChannelMaxPower(c, inverter->channel[c].MaxChannelPower);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +200,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("max_power"))) {
|
||||
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -225,8 +228,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray maxPowerArray = root[F("max_power")].as<JsonArray>();
|
||||
if (maxPowerArray.size() == 0 || maxPowerArray.size() > INV_MAX_CHAN_COUNT) {
|
||||
JsonArray channelArray = root[F("channel")].as<JsonArray>();
|
||||
if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) {
|
||||
retMsg[F("message")] = F("Invalid amount of max channel setting given!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -243,8 +246,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
strncpy(inverter.Name, root[F("name")].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
|
||||
uint8_t arrayCount = 0;
|
||||
for (JsonVariant maxPower : maxPowerArray) {
|
||||
inverter.MaxChannelPower[arrayCount] = maxPower.as<uint16_t>();
|
||||
for (JsonVariant channel : channelArray) {
|
||||
inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as<uint16_t>();
|
||||
strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name));
|
||||
arrayCount++;
|
||||
}
|
||||
|
||||
@ -272,7 +276,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
|
||||
if (inv != nullptr) {
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
inv->Statistics()->setChannelMaxPower(c, inverter.MaxChannelPower[c]);
|
||||
inv->Statistics()->setChannelMaxPower(c, inverter.channel[c].MaxChannelPower);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "WebApi_ws_live.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsLiveClass::WebApiWsLiveClass()
|
||||
: _ws("/livedata")
|
||||
@ -102,6 +103,12 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
|
||||
// Loop all channels
|
||||
for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) {
|
||||
if (c > 0) {
|
||||
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
|
||||
if (inv_cfg != nullptr) {
|
||||
invObject[String(c)][F("name")]["u"] = inv_cfg->channel[c - 1].Name;
|
||||
}
|
||||
}
|
||||
addField(invObject, i, inv, c, FLD_PAC);
|
||||
addField(invObject, i, inv, c, FLD_UAC);
|
||||
addField(invObject, i, inv, c, FLD_IAC);
|
||||
@ -119,8 +126,10 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
addField(invObject, i, inv, c, FLD_PF);
|
||||
addField(invObject, i, inv, c, FLD_PRA);
|
||||
addField(invObject, i, inv, c, FLD_EFF);
|
||||
if (c > 0 && inv->Statistics()->getChannelMaxPower(c - 1) > 0) {
|
||||
addField(invObject, i, inv, c, FLD_IRR);
|
||||
}
|
||||
}
|
||||
|
||||
if (inv->Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) {
|
||||
invObject[F("events")] = inv->EventLog()->getEntryCount();
|
||||
@ -142,6 +151,16 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
addTotalField(totalObj, "Power", totalPower, "W", 1);
|
||||
addTotalField(totalObj, "YieldDay", totalYieldDay, "Wh", 0);
|
||||
addTotalField(totalObj, "YieldTotal", totalYieldTotal, "kWh", 2);
|
||||
|
||||
JsonObject hintObj = root.createNestedObject("hints");
|
||||
struct tm timeinfo;
|
||||
hintObj[F("time_sync")] = !getLocalTime(&timeinfo, 5);
|
||||
hintObj[F("radio_problem")] = (!Hoymiles.getRadio()->isConnected() || !Hoymiles.getRadio()->isPVariant());
|
||||
if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) {
|
||||
hintObj[F("default_password")] = true;
|
||||
} else {
|
||||
hintObj[F("default_password")] = false;
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, String topic)
|
||||
|
||||
@ -119,7 +119,7 @@ void setup()
|
||||
|
||||
if (inv != nullptr) {
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].MaxChannelPower[c]);
|
||||
inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower);
|
||||
}
|
||||
}
|
||||
Serial.println(F(" done"));
|
||||
|
||||
@ -27,12 +27,12 @@
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.3",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^3.2.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-css-injected-by-js": "^2.1.1",
|
||||
"vue-tsc": "^1.0.9"
|
||||
|
||||
50
webapp/src/components/HintView.vue
Normal file
50
webapp/src/components/HintView.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<BootstrapAlert :show="hints.radio_problem" variant="danger">
|
||||
<BIconBroadcast class="fs-4" /> Could not connect to a correct NRF24L01+ radio module. Please check the wiring.
|
||||
</BootstrapAlert>
|
||||
|
||||
<BootstrapAlert :show="hints.time_sync" variant="danger">
|
||||
<BIconClock class="fs-4" /> The clock has not yet been synchronised. Without a
|
||||
correctly set clock, no requests are made to the inverter. This is normal shortly after the start. However,
|
||||
after a longer runtime (>1 minute), it indicates that the NTP server is not accessible. <a
|
||||
@click="gotoTimeSettings" href="#" class="alert-link">Please check your time
|
||||
settings.</a>
|
||||
</BootstrapAlert>
|
||||
|
||||
<BootstrapAlert :show="hints.default_password" variant="danger">
|
||||
<BIconExclamationCircle class="fs-4" /> You are using the default password for the web interface and the
|
||||
emergency access point. This is potentially insecure. <a @click="gotoPasswordSettings" href="#"
|
||||
class="alert-link">Please change the password.</a>
|
||||
</BootstrapAlert>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import type { Hints } from '@/types/LiveDataStatus';
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import {
|
||||
BIconClock,
|
||||
BIconExclamationCircle,
|
||||
BIconBroadcast,
|
||||
} from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
BIconClock,
|
||||
BIconExclamationCircle,
|
||||
BIconBroadcast,
|
||||
},
|
||||
props: {
|
||||
hints: { type: Object as PropType<Hints>, required: true },
|
||||
},
|
||||
methods: {
|
||||
gotoTimeSettings() {
|
||||
this.$router.push("/settings/ntp");
|
||||
},
|
||||
gotoPasswordSettings() {
|
||||
this.$router.push("/settings/security");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="card" :class="{ 'border-info': channelNumber == 0 }">
|
||||
<div v-if="channelNumber >= 1" class="card-header">String {{ channelNumber }}</div>
|
||||
<div v-if="channelNumber >= 1" class="card-header">
|
||||
<template v-if="channelData.name.u != ''">{{ channelData.name.u }}</template>
|
||||
<template v-else>String {{ channelNumber }}</template>
|
||||
</div>
|
||||
<div v-if="channelNumber == 0" class="card-header text-bg-info">Phase {{ channelNumber + 1 }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
@ -13,7 +16,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(property, key) in channelData" :key="`prop-${key}`">
|
||||
<template v-if="property">
|
||||
<template v-if="key != 'name' && property">
|
||||
<th scope="row">{{ key }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(property.v, property.d) }}</td>
|
||||
<td>{{ property.u }}</td>
|
||||
|
||||
@ -5,6 +5,7 @@ export interface ValueObject {
|
||||
};
|
||||
|
||||
export interface InverterStatistics {
|
||||
name: ValueObject,
|
||||
Power?: ValueObject;
|
||||
Voltage?: ValueObject;
|
||||
Current?: ValueObject;
|
||||
@ -37,7 +38,14 @@ export interface Total {
|
||||
YieldTotal: ValueObject;
|
||||
};
|
||||
|
||||
export interface Hints {
|
||||
time_sync: boolean;
|
||||
default_password: boolean;
|
||||
radio_problem: boolean;
|
||||
};
|
||||
|
||||
export interface LiveData {
|
||||
inverters: Inverter[];
|
||||
total: Total;
|
||||
hints: Hints;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<BasePage :title="'Live Data'" :isLoading="dataLoading" :isWideScreen="true">
|
||||
<HintView :hints="liveData.hints" />
|
||||
<InverterTotalInfo :totalData="liveData.total" /><br />
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]">
|
||||
@ -45,6 +46,9 @@
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
Data Age: {{ inverter.data_age }} seconds
|
||||
<template v-if="inverter.data_age > 300">
|
||||
/ {{ calculateAbsoluteTime(inverter.data_age) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -334,6 +338,7 @@ import DevInfo from '@/components/DevInfo.vue';
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import InverterChannelInfo from "@/components/InverterChannelInfo.vue";
|
||||
import InverterTotalInfo from '@/components/InverterTotalInfo.vue';
|
||||
import HintView from '@/components/HintView.vue';
|
||||
import VedirectView from '@/views/VedirectView.vue';
|
||||
import type { DevInfoStatus } from '@/types/DevInfoStatus';
|
||||
import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
@ -348,6 +353,7 @@ export default defineComponent({
|
||||
BasePage,
|
||||
InverterChannelInfo,
|
||||
InverterTotalInfo,
|
||||
HintView,
|
||||
EventLog,
|
||||
DevInfo,
|
||||
BootstrapAlert,
|
||||
@ -664,6 +670,11 @@ export default defineComponent({
|
||||
}
|
||||
)
|
||||
},
|
||||
calculateAbsoluteTime(lastTime: number): string {
|
||||
const userLocale = globalThis.navigator.language;
|
||||
const date = new Date(Date.now() - lastTime * 1000);
|
||||
return date.toLocaleString(userLocale)
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -73,25 +73,38 @@
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-serial" class="col-form-label">Serial:</label>
|
||||
<label for="inverter-serial" class="col-form-label">Inverter Serial:</label>
|
||||
<input v-model="selectedInverterData.serial" type="number" id="inverter-serial"
|
||||
class="form-control" />
|
||||
<label for="inverter-name" class="col-form-label">Name:</label>
|
||||
<label for="inverter-name" class="col-form-label">Inverter Name:</label>
|
||||
<input v-model="selectedInverterData.name" type="text" id="inverter-name"
|
||||
class="form-control" maxlength="31" />
|
||||
</div>
|
||||
|
||||
<div v-for="(max, index) in selectedInverterData.max_power" :key="`${index}`">
|
||||
<div v-for="(max, index) in selectedInverterData.channel" :key="`${index}`">
|
||||
<div class="row g-2">
|
||||
<div class="col-md">
|
||||
<label :for="`inverter-name_${index}`" class="col-form-label">Name string {{ index +1 }}:</label>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" :id="`inverter-name_${index}`" maxlength="31"
|
||||
v-model="selectedInverterData.channel[index].name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +1 }}:</label>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
|
||||
v-model="selectedInverterData.max_power[index]"
|
||||
v-model="selectedInverterData.channel[index].max_power"
|
||||
:aria-describedby="`inverter-maxDescription_${index} inverter-customizer`" />
|
||||
<span class="input-group-text" :id="`inverter-maxDescription_${index}`">W<sup>*</sup></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :id="`inverter-customizer`" class="form-text">*) Input the kWp of the channel to
|
||||
calculate irradiation.</div>
|
||||
</form>
|
||||
@ -139,12 +152,17 @@ import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
|
||||
declare interface Channel {
|
||||
name: string;
|
||||
max_power: number;
|
||||
}
|
||||
|
||||
declare interface Inverter {
|
||||
id: string;
|
||||
serial: number;
|
||||
name: string;
|
||||
type: string;
|
||||
max_power: number[];
|
||||
channel: Array<Channel>;
|
||||
}
|
||||
|
||||
declare interface AlertResponse {
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<div class="row mb-3">
|
||||
<label for="inputUsername" class="col-sm-2 col-form-label">Username:</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputUsername" maxlength="32"
|
||||
<input type="text" class="form-control" id="inputUsername" maxlength="64"
|
||||
placeholder="Username, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_username" />
|
||||
</div>
|
||||
@ -64,7 +64,7 @@
|
||||
<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="32"
|
||||
<input type="password" class="form-control" id="inputPassword" maxlength="64"
|
||||
placeholder="Password, leave empty for anonymous connection"
|
||||
v-model="mqttConfigList.mqtt_password" />
|
||||
</div>
|
||||
|
||||
@ -924,10 +924,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.27.0:
|
||||
version "8.27.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.27.0.tgz#d547e2f7239994ad1faa4bb5d84e5d809db7cf64"
|
||||
integrity sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==
|
||||
eslint@^8.28.0:
|
||||
version "8.28.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e"
|
||||
integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.3"
|
||||
"@humanwhocodes/config-array" "^0.11.6"
|
||||
@ -2060,10 +2060,10 @@ type-fest@^0.20.2:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||
|
||||
typescript@^4.8.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
|
||||
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
|
||||
typescript@^4.9.3:
|
||||
version "4.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db"
|
||||
integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
@ -2114,10 +2114,10 @@ vite-plugin-css-injected-by-js@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.1.1.tgz#a79275241c61f1c8d55d228f5b2dded450a580e4"
|
||||
integrity sha512-gjIG6iFWde32oRr/bK9CsfN+jdbura2y4GlDzeOiEm6py38ud8dXzFl9zwmHjOjZdr8XEgQ9TovzVGNzp47esw==
|
||||
|
||||
vite@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.3.tgz#7a68d9ef73eff7ee6dc0718ad3507adfc86944a7"
|
||||
integrity sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==
|
||||
vite@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e"
|
||||
integrity sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==
|
||||
dependencies:
|
||||
esbuild "^0.15.9"
|
||||
postcss "^8.4.18"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user