diff --git a/README.md b/README.md index fed9fc77..b8ec39b8 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,8 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md) ## Available cases -* [https://www.thingiverse.com/thing:5435911](https://www.thingiverse.com/thing:5435911) +* +* ## Building * Building the WebApp diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 7ee28662..7cc1c418 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -68,4 +68,5 @@ cmd topics are used to set values. Status topics are updated from values set in | [serial]/cmd/limit_persistent_absolute | W | Set the inverter limit as a absolute value. The value will survive the night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) | | [serial]/cmd/limit_nonpersistent_relative | W | Set the inverter limit as a percentage of total production capability. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic immediatly. | % | | [serial]/cmd/limit_nonpersistent_absolute | W | Set the inverter limit as a absolute value. The value will reset to the last persistent value at night without power. The updated value will show up in the web GUI and limit_relative topic after around 4 minutes. | Watt (W) | -| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 | \ No newline at end of file +| [serial]/cmd/power | W | Turn the inverter on (1) or off (0) | 0 or 1 | +| [serial]/cmd/restart | W | Restarts the inverters (also resets YieldDay) | 1 | \ No newline at end of file diff --git a/docs/screenshots/01_LiveView.png b/docs/screenshots/01_LiveView.png index 1291342a..3121a268 100644 Binary files a/docs/screenshots/01_LiveView.png and b/docs/screenshots/01_LiveView.png differ diff --git a/docs/screenshots/02_NetworkAdmin.png b/docs/screenshots/02_NetworkAdmin.png index 878b6b01..2e1505e2 100644 Binary files a/docs/screenshots/02_NetworkAdmin.png and b/docs/screenshots/02_NetworkAdmin.png differ diff --git a/docs/screenshots/03_NtpAdmin.png b/docs/screenshots/03_NtpAdmin.png index 7efba5de..92a9228b 100644 Binary files a/docs/screenshots/03_NtpAdmin.png and b/docs/screenshots/03_NtpAdmin.png differ diff --git a/docs/screenshots/05_InverterAdmin.png b/docs/screenshots/05_InverterAdmin.png index 9e4d0f58..d794a3b2 100644 Binary files a/docs/screenshots/05_InverterAdmin.png and b/docs/screenshots/05_InverterAdmin.png differ diff --git a/docs/screenshots/06_DtuAdmin.png b/docs/screenshots/06_DtuAdmin.png index 4e74f68d..67d6dd03 100644 Binary files a/docs/screenshots/06_DtuAdmin.png and b/docs/screenshots/06_DtuAdmin.png differ diff --git a/docs/screenshots/08_NetworkInfo.png b/docs/screenshots/08_NetworkInfo.png index 18f61869..e667ea23 100644 Binary files a/docs/screenshots/08_NetworkInfo.png and b/docs/screenshots/08_NetworkInfo.png differ diff --git a/docs/screenshots/10_MqttInfo.png b/docs/screenshots/10_MqttInfo.png index 098bd2a7..2ab58f3e 100644 Binary files a/docs/screenshots/10_MqttInfo.png and b/docs/screenshots/10_MqttInfo.png differ diff --git a/docs/screenshots/12_Eventlog.png b/docs/screenshots/12_Eventlog.png index 8d6c6878..2b34dd75 100644 Binary files a/docs/screenshots/12_Eventlog.png and b/docs/screenshots/12_Eventlog.png differ diff --git a/docs/screenshots/13_InverterSettings.png b/docs/screenshots/13_InverterSettings.png index 8b546758..be35ded4 100644 Binary files a/docs/screenshots/13_InverterSettings.png and b/docs/screenshots/13_InverterSettings.png differ diff --git a/docs/screenshots/14_ConfigManagement.png b/docs/screenshots/14_ConfigManagement.png index 109e3b7a..04c569b0 100644 Binary files a/docs/screenshots/14_ConfigManagement.png and b/docs/screenshots/14_ConfigManagement.png differ diff --git a/docs/screenshots/15_LimitSettings.png b/docs/screenshots/15_LimitSettings.png new file mode 100644 index 00000000..44c6f595 Binary files /dev/null and b/docs/screenshots/15_LimitSettings.png differ diff --git a/docs/screenshots/16_PowerSettings.png b/docs/screenshots/16_PowerSettings.png new file mode 100644 index 00000000..ee68fe5d Binary files /dev/null and b/docs/screenshots/16_PowerSettings.png differ diff --git a/docs/screenshots/17_InverterInfo.png b/docs/screenshots/17_InverterInfo.png new file mode 100644 index 00000000..da65f323 Binary files /dev/null and b/docs/screenshots/17_InverterInfo.png differ diff --git a/include/Configuration.h b/include/Configuration.h index 6dcdef7d..57c3233a 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,7 +4,8 @@ #include #define CONFIG_FILENAME "/config.bin" -#define CONFIG_VERSION 0x00011500 // 0.1.21 // make sure to clean all after change +#define CONFIG_FILENAME_JSON "/config.json" +#define CONFIG_VERSION 0x00011600 // 0.1.22 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -26,6 +27,8 @@ #define INV_MAX_COUNT 10 #define INV_MAX_CHAN_COUNT 4 +#define JSON_BUFFER_SIZE 6144 + struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; @@ -82,6 +85,8 @@ struct CONFIG_T { char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; bool Mqtt_Hass_Expire; + + char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; }; class ConfigurationClass { @@ -93,6 +98,9 @@ public: CONFIG_T& get(); INVERTER_CONFIG_T* getFreeInverterSlot(); + +private: + bool readJson(); }; extern ConfigurationClass Configuration; \ No newline at end of file diff --git a/include/WebApi.h b/include/WebApi.h index 6d59da71..44bed9b8 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -12,6 +12,7 @@ #include "WebApi_network.h" #include "WebApi_ntp.h" #include "WebApi_power.h" +#include "WebApi_security.h" #include "WebApi_sysstatus.h" #include "WebApi_webapp.h" #include "WebApi_ws_live.h" @@ -40,6 +41,7 @@ private: WebApiNetworkClass _webApiNetwork; WebApiNtpClass _webApiNtp; WebApiPowerClass _webApiPower; + WebApiSecurityClass _webApiSecurity; WebApiSysstatusClass _webApiSysstatus; WebApiWebappClass _webApiWebapp; WebApiWsLiveClass _webApiWsLive; diff --git a/include/WebApi_security.h b/include/WebApi_security.h new file mode 100644 index 00000000..d94a7eeb --- /dev/null +++ b/include/WebApi_security.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiSecurityClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onPasswordGet(AsyncWebServerRequest* request); + void onPasswordPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 8e26caa0..8cbc6e10 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -169,6 +169,11 @@ bool HoymilesRadio::isConnected() return _radio->isChipConnected(); } +bool HoymilesRadio::isPVariant() +{ + return _radio->isPVariant(); +} + void HoymilesRadio::openReadingPipe() { serial_u s; diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index b60468c9..1a29af93 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -47,6 +47,7 @@ public: bool isIdle(); bool isConnected(); + bool isPVariant(); template T* enqueCommand() diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 079c8642..5fc0bbb2 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -97,6 +97,10 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) { + if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) { + limit = min(100, limit); + } + _activePowerControlLimit = limit; _activePowerControlType = type; @@ -115,7 +119,11 @@ bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio) bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) { - _powerState = turnOn; + if (turnOn) { + _powerState = 1; + } else { + _powerState = 0; + } PowerControlCommand* cmd = radio->enqueCommand(); cmd->setPowerOn(turnOn); @@ -125,7 +133,33 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) return true; } +bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) +{ + _powerState = 2; + + PowerControlCommand* cmd = radio->enqueCommand(); + cmd->setRestart(); + cmd->setTargetAddress(serial()); + PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); + + return true; +} + bool HM_Abstract::resendPowerControlRequest(HoymilesRadio* radio) { - return sendPowerControlRequest(radio, _powerState); + switch (_powerState) { + case 0: + return sendPowerControlRequest(radio, false); + break; + case 1: + return sendPowerControlRequest(radio, true); + break; + case 2: + return sendRestartControlRequest(radio); + break; + + default: + return false; + break; + } } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index 8cd07562..da3e0d88 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -12,6 +12,7 @@ public: bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type); bool resendActivePowerControlRequest(HoymilesRadio* radio); bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn); + bool sendRestartControlRequest(HoymilesRadio* radio); bool resendPowerControlRequest(HoymilesRadio* radio); private: @@ -19,5 +20,5 @@ private: float _activePowerControlLimit = 0; PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent; - bool _powerState = true; + uint8_t _powerState = 1; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 4b9a2593..9e97939b 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -53,6 +53,7 @@ public: virtual bool sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) = 0; virtual bool resendActivePowerControlRequest(HoymilesRadio* radio) = 0; virtual bool sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) = 0; + virtual bool sendRestartControlRequest(HoymilesRadio* radio) = 0; virtual bool resendPowerControlRequest(HoymilesRadio* radio) = 0; AlarmLogParser* EventLog(); diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index 8ac43eaa..ed59588f 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -19,7 +19,7 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui float SystemConfigParaParser::getLimitPercent() { - return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10; + return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; } void SystemConfigParaParser::setLimitPercent(float value) diff --git a/platformio.ini b/platformio.ini index f5e5b3da..ad7a7943 100644 --- a/platformio.ini +++ b/platformio.ini @@ -31,7 +31,7 @@ extra_scripts = board_build.partitions = partitions_custom.csv board_build.filesystem = littlefs -monitor_filters = time, colorize, log2file, esp32_exception_decoder +monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 upload_protocol = esptool diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 11287bb8..7bc11b30 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -4,6 +4,7 @@ */ #include "Configuration.h" #include "defaults.h" +#include #include CONFIG_T config; @@ -58,6 +59,8 @@ void ConfigurationClass::init() strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS; + strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + config.Vedirect_Enabled = VEDIRECT_ENABLED; config.Vedirect_UpdatesOnly = VEDIRECT_UPDATESONLY; config.Vedirect_PollInterval = VEDIRECT_POLL_INTERVAL; @@ -65,29 +68,221 @@ void ConfigurationClass::init() bool ConfigurationClass::write() { - File f = LittleFS.open(CONFIG_FILENAME, "w"); + File f = LittleFS.open(CONFIG_FILENAME_JSON, "w"); if (!f) { return false; } config.Cfg_SaveCount++; - uint8_t* bytes = reinterpret_cast(&config); - for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { - f.write(bytes[i]); + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + + JsonObject cfg = doc.createNestedObject("cfg"); + cfg["version"] = config.Cfg_Version; + cfg["save_count"] = config.Cfg_SaveCount; + + JsonObject wifi = doc.createNestedObject("wifi"); + wifi["ssid"] = config.WiFi_Ssid; + wifi["password"] = config.WiFi_Password; + wifi["ip"] = IPAddress(config.WiFi_Ip).toString(); + wifi["netmask"] = IPAddress(config.WiFi_Netmask).toString(); + wifi["gateway"] = IPAddress(config.WiFi_Gateway).toString(); + wifi["dns1"] = IPAddress(config.WiFi_Dns1).toString(); + wifi["dns2"] = IPAddress(config.WiFi_Dns2).toString(); + wifi["dhcp"] = config.WiFi_Dhcp; + wifi["hostname"] = config.WiFi_Hostname; + + JsonObject ntp = doc.createNestedObject("ntp"); + ntp["server"] = config.Ntp_Server; + ntp["timezone"] = config.Ntp_Timezone; + ntp["timezone_descr"] = config.Ntp_TimezoneDescr; + + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["enabled"] = config.Mqtt_Enabled; + mqtt["hostname"] = config.Mqtt_Hostname; + mqtt["port"] = config.Mqtt_Port; + mqtt["username"] = config.Mqtt_Username; + mqtt["password"] = config.Mqtt_Password; + mqtt["topic"] = config.Mqtt_Topic; + mqtt["retain"] = config.Mqtt_Retain; + mqtt["publish_invterval"] = config.Mqtt_PublishInterval; + + JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); + mqtt_lwt["topic"] = config.Mqtt_LwtTopic; + mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; + mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; + + JsonObject mqtt_tls = mqtt.createNestedObject("tls"); + mqtt_tls["enabled"] = config.Mqtt_Tls; + mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; + + JsonObject mqtt_hass = mqtt.createNestedObject("hass"); + mqtt_hass["enabled"] = config.Mqtt_Hass_Enabled; + mqtt_hass["retain"] = config.Mqtt_Hass_Retain; + mqtt_hass["topic"] = config.Mqtt_Hass_Topic; + mqtt_hass["individual_panels"] = config.Mqtt_Hass_IndividualPanels; + mqtt_hass["expire"] = config.Mqtt_Hass_Expire; + + JsonObject dtu = doc.createNestedObject("dtu"); + dtu["serial"] = config.Dtu_Serial; + dtu["poll_interval"] = config.Dtu_PollInterval; + dtu["pa_level"] = config.Dtu_PaLevel; + + JsonObject security = doc.createNestedObject("security"); + security["password"] = config.Security_Password; + + JsonArray inverters = doc.createNestedArray("inverters"); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters.createNestedObject(); + inv["serial"] = config.Inverter[i].Serial; + inv["name"] = config.Inverter[i].Name; + + JsonArray channels = inv.createNestedArray("channels"); + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + channels.add(config.Inverter[i].MaxChannelPower[c]); + } } + + // Serialize JSON to file + if (serializeJson(doc, f) == 0) { + Serial.println("Failed to write file"); + return false; + } + f.close(); return true; } bool ConfigurationClass::read() { - File f = LittleFS.open(CONFIG_FILENAME, "r"); + if (!LittleFS.exists(CONFIG_FILENAME_JSON)) { + Serial.println("Converting binary config to json... "); + File f = LittleFS.open(CONFIG_FILENAME, "r"); + if (!f) { + return false; + } + uint8_t* bytes = reinterpret_cast(&config); + for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { + bytes[i] = f.read(); + } + f.close(); + write(); + Serial.println("done"); + LittleFS.remove(CONFIG_FILENAME); + } + return readJson(); +} + +bool ConfigurationClass::readJson() +{ + File f = LittleFS.open(CONFIG_FILENAME_JSON, "r", false); if (!f) { return false; } - uint8_t* bytes = reinterpret_cast(&config); - for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { - bytes[i] = f.read(); + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + Serial.println(F("Failed to read file, using default configuration")); } + + JsonObject cfg = doc["cfg"]; + config.Cfg_Version = cfg["version"] | CONFIG_VERSION; + config.Cfg_SaveCount = cfg["save_count"] | 0; + + JsonObject wifi = doc["wifi"]; + strlcpy(config.WiFi_Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi_Ssid)); + strlcpy(config.WiFi_Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi_Password)); + strlcpy(config.WiFi_Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi_Hostname)); + + IPAddress wifi_ip; + wifi_ip.fromString(wifi["ip"] | ""); + config.WiFi_Ip[0] = wifi_ip[0]; + config.WiFi_Ip[1] = wifi_ip[1]; + config.WiFi_Ip[2] = wifi_ip[2]; + config.WiFi_Ip[3] = wifi_ip[3]; + + IPAddress wifi_netmask; + wifi_netmask.fromString(wifi["netmask"] | ""); + config.WiFi_Netmask[0] = wifi_netmask[0]; + config.WiFi_Netmask[1] = wifi_netmask[1]; + config.WiFi_Netmask[2] = wifi_netmask[2]; + config.WiFi_Netmask[3] = wifi_netmask[3]; + + IPAddress wifi_gateway; + wifi_gateway.fromString(wifi["gateway"] | ""); + config.WiFi_Gateway[0] = wifi_gateway[0]; + config.WiFi_Gateway[1] = wifi_gateway[1]; + config.WiFi_Gateway[2] = wifi_gateway[2]; + config.WiFi_Gateway[3] = wifi_gateway[3]; + + IPAddress wifi_dns1; + wifi_dns1.fromString(wifi["dns1"] | ""); + config.WiFi_Dns1[0] = wifi_dns1[0]; + config.WiFi_Dns1[1] = wifi_dns1[1]; + config.WiFi_Dns1[2] = wifi_dns1[2]; + config.WiFi_Dns1[3] = wifi_dns1[3]; + + IPAddress wifi_dns2; + wifi_dns2.fromString(wifi["dns2"] | ""); + config.WiFi_Dns2[0] = wifi_dns2[0]; + config.WiFi_Dns2[1] = wifi_dns2[1]; + config.WiFi_Dns2[2] = wifi_dns2[2]; + config.WiFi_Dns2[3] = wifi_dns2[3]; + + config.WiFi_Dhcp = wifi["dhcp"] | WIFI_DHCP; + + JsonObject ntp = doc["ntp"]; + strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); + strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); + strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); + + JsonObject mqtt = doc["mqtt"]; + config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; + strlcpy(config.Mqtt_Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt_Hostname)); + config.Mqtt_Port = mqtt["port"] | MQTT_PORT; + strlcpy(config.Mqtt_Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt_Username)); + strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); + strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); + config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; + config.Mqtt_PublishInterval = mqtt["publish_invterval"] | MQTT_PUBLISH_INTERVAL; + + JsonObject mqtt_lwt = mqtt["lwt"]; + strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); + strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); + strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + + JsonObject mqtt_tls = mqtt["tls"]; + config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; + strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); + + JsonObject mqtt_hass = mqtt["hass"]; + config.Mqtt_Hass_Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED; + config.Mqtt_Hass_Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN; + config.Mqtt_Hass_Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE; + config.Mqtt_Hass_IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS; + strlcpy(config.Mqtt_Hass_Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); + + JsonObject dtu = doc["dtu"]; + config.Dtu_Serial = dtu["serial"] | DTU_SERIAL; + config.Dtu_PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL; + config.Dtu_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL; + + JsonObject security = doc["security"]; + strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + + JsonArray inverters = doc["inverters"]; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters[i].as(); + config.Inverter[i].Serial = inv["serial"] | 0ULL; + strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + + JsonArray channels = inv["channels"]; + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + config.Inverter[i].MaxChannelPower[c] = channels[c]; + } + } + f.close(); return true; } @@ -163,6 +358,10 @@ void ConfigurationClass::migrate() config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE; } + if (config.Cfg_Version < 0x00011600) { + strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + } + config.Cfg_Version = CONFIG_VERSION; write(); } diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index a874176e..d7e7a655 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -15,6 +15,7 @@ #define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative" #define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute" #define TOPIC_SUB_POWER "power" +#define TOPIC_SUB_RESTART "restart" MqttSettingsClass::MqttSettingsClass() { @@ -48,6 +49,7 @@ void MqttSettingsClass::onMqttConnect(bool sessionPresent) mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0); + mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0); } void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) @@ -127,7 +129,6 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { // Set inverter limit relative persistent - payload_val = min(100, payload_val); Serial.printf("Limit Persistent: %d %%\n", payload_val); inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativPersistent); @@ -138,7 +139,6 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { // Set inverter limit relative non persistent - payload_val = min(100, payload_val); Serial.printf("Limit Non-Persistent: %d %%\n", payload_val); if (!properties.retain) { inv->sendActivePowerControlRequest(Hoymiles.getRadio(), payload_val, PowerLimitControlType::RelativNonPersistent); @@ -155,10 +155,19 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie Serial.println("Ignored because retained"); } - } else if(!strcmp(setting, TOPIC_SUB_POWER)) { + } else if (!strcmp(setting, TOPIC_SUB_POWER)) { // Turn inverter on or off Serial.printf("Set inverter power to: %d\n", payload_val); inv->sendPowerControlRequest(Hoymiles.getRadio(), payload_val > 0); + + } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { + // Restart inverter + Serial.printf("Restart inverter\n"); + if (!properties.retain && payload_val == 1) { + inv->sendRestartControlRequest(Hoymiles.getRadio()); + } else { + Serial.println("Ignored because retained"); + } } } diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 98e3a6a6..f4df6da3 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -115,7 +115,7 @@ void NetworkSettingsClass::setupMode() WiFi.mode(WIFI_AP_STA); String ssidString = getApName(); WiFi.softAPConfig(apIp, apIp, apNetmask); - WiFi.softAP((const char*)ssidString.c_str(), ACCESS_POINT_PASSWORD); + WiFi.softAP((const char*)ssidString.c_str(), Configuration.get().Security_Password); dnsServer->setErrorReplyCode(DNSReplyCode::NoError); dnsServer->start(DNS_PORT, "*", WiFi.softAPIP()); dnsServerStatus = true; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 91aa936c..dcc60dff 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -28,6 +28,7 @@ void WebApiClass::init() _webApiNetwork.init(&_server); _webApiNtp.init(&_server); _webApiPower.init(&_server); + _webApiSecurity.init(&_server); _webApiSysstatus.init(&_server); _webApiWebapp.init(&_server); _webApiWsLive.init(&_server); @@ -50,6 +51,7 @@ void WebApiClass::loop() _webApiNetwork.loop(); _webApiNtp.loop(); _webApiPower.loop(); + _webApiSecurity.loop(); _webApiSysstatus.loop(); _webApiWebapp.loop(); _webApiWsLive.loop(); diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 7bda7133..e4376432 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -32,7 +32,7 @@ void WebApiConfigClass::loop() void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) { - request->send(LittleFS, CONFIG_FILENAME, String(), true); + request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true); } void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) @@ -87,7 +87,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) response->setLength(); request->send(response); - LittleFS.remove(CONFIG_FILENAME); + LittleFS.remove(CONFIG_FILENAME_JSON); ESP.restart(); } @@ -110,7 +110,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi { if (!index) { // open the file on first call and store the file handle in the request object - request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w"); + request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w"); } if (len) { diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index fda74c55..cfcb21e7 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -36,6 +36,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) ((uint32_t)(inv->serial() & 0xFFFFFFFF))); root[buffer]["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); + root[buffer]["max_power"] = inv->DevInfo()->getMaxPower(); LastCommandSuccess status = inv->SystemConfigPara()->getLastLimitCommandSuccess(); String limitStatus = "Unknown"; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 33db93c3..78cbbd0b 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -170,6 +170,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } + if (!root[F("mqtt_topic")].as().endsWith("/")) { + retMsg[F("message")] = F("Topic must end with slash (/)!"); + response->setLength(); + request->send(response); + return; + } + if (root[F("mqtt_port")].as() == 0 || root[F("mqtt_port")].as() > 65535) { retMsg[F("message")] = F("Port must be a number between 1 and 65535!"); response->setLength(); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 7c950604..27760f9d 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -151,11 +151,13 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) request->send(response); return; } - if (root[F("ssid")].as().length() == 0 || root[F("ssid")].as().length() > WIFI_MAX_SSID_STRLEN) { - retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; + if (NetworkSettings.NetworkMode() == network_mode::WiFi) { + if (root[F("ssid")].as().length() == 0 || root[F("ssid")].as().length() > WIFI_MAX_SSID_STRLEN) { + retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } } if (root[F("password")].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 30aacf80..8ecda811 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -39,11 +39,9 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) String limitStatus = "Unknown"; if (status == LastCommandSuccess::CMD_OK) { limitStatus = "Ok"; - } - else if (status == LastCommandSuccess::CMD_NOK) { + } else if (status == LastCommandSuccess::CMD_NOK) { limitStatus = "Failure"; - } - else if (status == LastCommandSuccess::CMD_PENDING) { + } else if (status == LastCommandSuccess::CMD_PENDING) { limitStatus = "Pending"; } root[buffer]["power_set_status"] = limitStatus; @@ -86,7 +84,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } if (!(root.containsKey("serial") - && root.containsKey("power"))) { + && (root.containsKey("power") || root.containsKey("restart")))) { retMsg[F("message")] = F("Values are missing!"); response->setLength(); request->send(response); @@ -101,8 +99,6 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } uint64_t serial = strtoll(root[F("serial")].as().c_str(), NULL, 16); - uint16_t power = root[F("power")].as(); - auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg[F("message")] = F("Invalid inverter specified!"); @@ -111,7 +107,14 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - inv->sendPowerControlRequest(Hoymiles.getRadio(), power); + if (root.containsKey("power")) { + uint16_t power = root[F("power")].as(); + inv->sendPowerControlRequest(Hoymiles.getRadio(), power); + } else { + if (root[F("restart")].as()) { + inv->sendRestartControlRequest(Hoymiles.getRadio()); + } + } retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp new file mode 100644 index 00000000..2009be96 --- /dev/null +++ b/src/WebApi_security.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_security.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "helper.h" + +void WebApiSecurityClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/security/password", HTTP_GET, std::bind(&WebApiSecurityClass::onPasswordGet, this, _1)); + _server->on("/api/security/password", HTTP_POST, std::bind(&WebApiSecurityClass::onPasswordPost, this, _1)); +} + +void WebApiSecurityClass::loop() +{ +} + +void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + root[F("password")] = config.Security_Password; + + response->setLength(); + request->send(response); +} + +void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!root.containsKey("password")) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("password")].as().length() < 8 || root[F("password")].as().length() > WIFI_MAX_PASSWORD_STRLEN) { + retMsg[F("message")] = F("Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + strlcpy(config.Security_Password, root[F("password")].as().c_str(), sizeof(config.Security_Password)); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); +} \ No newline at end of file diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 36f71edf..2d6b2987 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -66,6 +66,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root[F("uptime")] = esp_timer_get_time() / 1000000; root[F("radio_connected")] = Hoymiles.getRadio()->isConnected(); + root[F("radio_pvariant")] = Hoymiles.getRadio()->isPVariant(); response->setLength(); request->send(response); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ab098fac..032c9078 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -87,6 +87,12 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) root[i][F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; root[i][F("reachable")] = inv->isReachable(); root[i][F("producing")] = inv->isProducing(); + root[i][F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent(); + if (inv->DevInfo()->getMaxPower() > 0) { + root[i][F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; + } else { + root[i][F("limit_absolute")] = -1; + } // Loop all channels for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { diff --git a/webapp/package.json b/webapp/package.json index 4f5d9b92..19712d1d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -21,7 +21,7 @@ "@babel/core": "^7.19.3", "@babel/eslint-parser": "^7.19.1", "@types/bootstrap": "^5.2.5", - "@types/node": "^18.8.2", + "@types/node": "^18.8.3", "@types/spark-md5": "^3.0.2", "@typescript-eslint/parser": "^5.38.1", "@vue/cli-plugin-babel": "~5.0.8", @@ -30,7 +30,7 @@ "@vue/cli-plugin-typescript": "^5.0.8", "@vue/cli-service": "~5.0.8", "@vue/eslint-config-typescript": "^11.0.2", - "eslint": "^8.24.0", + "eslint": "^8.25.0", "eslint-plugin-vue": "^9.6.0", "typescript": "^4.8.4", "vue-cli-plugin-compression": "~2.0.0" diff --git a/webapp/src/components/AboutView.vue b/webapp/src/components/AboutView.vue index 7196cf52..77f897b8 100644 --- a/webapp/src/components/AboutView.vue +++ b/webapp/src/components/AboutView.vue @@ -98,4 +98,24 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/webapp/src/components/ConfigAdminView.vue b/webapp/src/components/ConfigAdminView.vue index 04d15caa..aacad9a6 100644 --- a/webapp/src/components/ConfigAdminView.vue +++ b/webapp/src/components/ConfigAdminView.vue @@ -55,7 +55,7 @@
- +
@@ -116,11 +116,19 @@ \ No newline at end of file diff --git a/webapp/src/components/SystemInfoView.vue b/webapp/src/components/SystemInfoView.vue index d0284782..28912131 100644 --- a/webapp/src/components/SystemInfoView.vue +++ b/webapp/src/components/SystemInfoView.vue @@ -67,6 +67,7 @@ export default defineComponent({ sketch_used: 0, // RadioInfo radio_connected: false, + radio_pvariant: false, } } }, diff --git a/webapp/src/components/partials/DevInfo.vue b/webapp/src/components/partials/DevInfo.vue index bb20e68d..ce53af49 100644 --- a/webapp/src/components/partials/DevInfo.vue +++ b/webapp/src/components/partials/DevInfo.vue @@ -10,7 +10,8 @@ Model {{ devInfoList.hw_model_name }} Unknown model! Please report the "Hardware Part Number" and model (e.g. HM-350) as an issue - here. + here. + Bootloader Version @@ -38,6 +39,7 @@ diff --git a/webapp/src/main.ts b/webapp/src/main.ts index 78162676..e54f91b1 100644 --- a/webapp/src/main.ts +++ b/webapp/src/main.ts @@ -1,9 +1,8 @@ import { createApp } from 'vue' import App from './App.vue' import router from './router' -import { BootstrapIconsPlugin } from 'bootstrap-icons-vue'; import "bootstrap/dist/css/bootstrap.min.css" import "bootstrap" -createApp(App).use(router).use(BootstrapIconsPlugin).mount('#app') +createApp(App).use(router).mount('#app') diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index bbc47e9e..af84f783 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -14,6 +14,7 @@ import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue' import ConfigAdminView from '@/components/ConfigAdminView.vue' import VedirectAdminView from '@/components/VedirectAdminView.vue' import VedirectInfoView from '@/components/VedirectInfoView.vue' +import SecurityAdminView from '@/components/SecurityAdminView.vue' const routes: Array = [ { @@ -90,6 +91,11 @@ const routes: Array = [ path: '/settings/config', name: 'Config Management', component: ConfigAdminView + }, + { + path: '/settings/security', + name: 'Security', + component: SecurityAdminView } ]; diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 7120287e..d148700f 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -1164,10 +1164,10 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@eslint/eslintrc@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" - integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== +"@eslint/eslintrc@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -1200,11 +1200,6 @@ debug "^4.1.1" minimatch "^3.0.4" -"@humanwhocodes/gitignore-to-minimatch@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" - integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" @@ -1469,10 +1464,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39" integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg== -"@types/node@^18.8.2": - version "18.8.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067" - integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA== +"@types/node@^18.8.3": + version "18.8.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c" + integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -3410,14 +3405,13 @@ eslint-webpack-plugin@^3.1.0: normalize-path "^3.0.0" schema-utils "^3.1.1" -eslint@^8.24.0: - version "8.24.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8" - integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ== +eslint@^8.25.0: + version "8.25.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.25.0.tgz#00eb962f50962165d0c4ee3327708315eaa8058b" + integrity sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A== dependencies: - "@eslint/eslintrc" "^1.3.2" + "@eslint/eslintrc" "^1.3.3" "@humanwhocodes/config-array" "^0.10.5" - "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" "@humanwhocodes/module-importer" "^1.0.1" ajv "^6.10.0" chalk "^4.0.0"