diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..4f358b10 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 🤔 Have questions or need support? + url: https://discord.gg/WzhxEY62mB + about: Discuss with us on Discord + - name: 🤔 Have questions or need support? + url: https://github.com/tbnobody/OpenDTU/discussions + about: Use the GitHub Discussions feature \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84b41adf..b4a6f403 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: - name: Get default environments id: envs run: | - echo "::set-output name=environments::$(pio project config --json-output | jq -cr '.[0][1][0][1]')" + echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT outputs: environments: ${{ steps.envs.outputs.environments }} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ed596687 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "C_Cpp.clang_format_style": "WebKit" +} \ No newline at end of file diff --git a/README.md b/README.md index 084b52c1..36f764a2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Sends text raw data as difined in VE.Direct spec. * Time zone support * Ve.Direct interface (via web-interface, REST-api, or MQTT) * Ethernet support +* Prometheus API endpoint (/api/prometheus/metrics) ## Features for developers * The microcontroller part @@ -206,7 +207,7 @@ Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/release ## 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) -* Navigate to Settings --> Network Settings and enter your WiFi credentials +* Navigate to Settings --> Network Settings and enter your WiFi credentials. The username to access the config menu is "admin" and the password the same as for accessing the Access Point (default: "openDTU42"). * OpenDTU then simultaneously connects to your WiFi AP with this credentials. Navigate to Info --> Network and look into section "Network Interface (Station)" for the IP address received via DHCP. * When OpenDTU is connected to a configured WiFI AP, the "OpenDTU-*" Access Point is closed after 3 minutes. * OpenDTU needs access to a working NTP server to get the current date & time. Both are sent to the inverter with each request. Default NTP server is pool.ntp.org. If your network has different requirements please change accordingly (Settings --> NTP Settings). @@ -225,6 +226,9 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw ## MQTT Topic Documentation A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md) +## Web API Documentation +A documentation of the Web API can be found here: [Web-API Documentation](docs/Web-API.md) + ## Available cases * * diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..eac94f93 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +# Documents - Table of content + +More detailed descriptions for some topics can be found here. + +## [MQTT Topic Documentation](MQTT_Topics.md) +## [Web API Documentation](Web-API.md) diff --git a/docs/Web-API.md b/docs/Web-API.md new file mode 100644 index 00000000..cd640782 --- /dev/null +++ b/docs/Web-API.md @@ -0,0 +1,460 @@ +# Web API + +Information in JSON format can be obtained through the web API + +## List of URLs +may be incomplete + +| GET/POST | Auth required | URL | +| -------- | --- | -- | +| Get | yes | /api/config/get | +| Post | yes | /api/config/delete | +| Post | yes | /api/config/upload | +| Get | no | /api/devinfo/status | +| Get+Post | yes | /api/dtu/config | +| Get | no | /api/eventlog/status?inv=inverter-serialnumber | +| Post | yes | /api/firmware/update | +| Get | yes | /api/inverter/list | +| Post | yes | /api/inverter/add | +| Post | yes | /api/inverter/del | +| Post | yes | /api/inverter/edit | +| Post | yes | /api/limit/config | +| Get | no | /api/limit/status | +| Get | no | /api/livedata/status | +| Get+Post | yes | /api/mqtt/config | +| Get | no | /api/mqtt/status | +| Get+Post | yes | /api/network/config | +| Get | no | /api/network/status | +| Get+Post | yes | /api/ntp/config | +| Get | no | /api/ntp/status | +| Get+Post | yes | /api/ntp/time | +| Get | no | /api/power/status | +| Post | yes | /api/power/config | +| Get+Post | yes | /api/security/password | +| Get | no | /api/system/status | + + +## Examples of Use + +### Important notes: + +- IP addresses and serial numbers in this examples are anonymized. Adjust to your own needs. +- The output from curl is without a linefeed at the end, so please be careful when copying the output - do not accidentally add the shell prompt directly after it. +- When POSTing config data to OpenDTU, always send all settings back, even if only one setting was changed. Sending single settings is not supported and you will receive a response `{"type":"warning","message":"Values are missing!"}` +- When POSTing, always put single quotes around the data part. Do not confuse the single quote `'` with the backtick `` ` ``. You have been warned. +- Some API calls have a single URL for GET and POST - e.g. `/api/ntp/config` +- Other API calls use e.g. `/api/limit/status` to GET data and a different URL `/api/limit/config` to POST data. +- If you want to investigate the web api communication, a good tool is [Postman](https://www.postman.com/) +- Settings API require username and password provided with Basic Authentication credentials + + +### Get information + +You can "talk" to the OpenDTU with a command line tool like `curl`. The output is in plain JSON, without carriage return/linefeed and is therefore not very human readable. + +#### Get current livedata + +``` +~$ curl http://192.168.10.10/api/livedata/status +{"inverters":[{"serial":"11418186xxxx","name":"HM600","data_age":4,"reachable":true,"producing":true,"limit_relative":100,"limit_absolute":600,"0":{"Power":{"v":70.69999695,"u":"W","d":1},"Voltage":{"v":233,"u":"V","d":1},"Current":{"v":0.300000012,"u":"A","d":2},"Power DC":{"v":74,"u":"W","d":2},"YieldDay":{"v":23,"u":"Wh","d":2},"YieldTotal":{"v":150.5050049,"u":"kWh","d":2},"Frequency":{"v":50.02000046,"u":"Hz","d":2},"Temperature":{"v":8.300000191,"u":"°C","d":1},"PowerFactor":{"v":1,"u":"","d":3},"ReactivePower":{"v":0.100000001,"u":"var","d":1},"Efficiency":{"v":95.54053497,"u":"%","d":2}},"1":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":1,"u":"V","d":1},"Current":{"v":0.02,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":49.0320015,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":2}},"2":{"Power":{"v":74,"u":"W","d":1},"Voltage":{"v":42.40000153,"u":"V","d":1},"Current":{"v":1.74000001,"u":"A","d":2},"YieldDay":{"v":23,"u":"Wh","d":0},"YieldTotal":{"v":101.4729996,"u":"kWh","d":3},"Irradiation":{"v":18.04878044,"u":"%","d":2}},"events":3},{"serial":"11418180xxxx","name":"HM800","data_age":11,"reachable":true,"producing":true,"limit_relative":100,"limit_absolute":800,"0":{"Power":{"v":70.09999847,"u":"W","d":1},"Voltage":{"v":233.1000061,"u":"V","d":1},"Current":{"v":0.300000012,"u":"A","d":2},"Power DC":{"v":73.59999847,"u":"W","d":2},"YieldDay":{"v":48,"u":"Wh","d":2},"YieldTotal":{"v":48.5399971,"u":"kWh","d":2},"Frequency":{"v":50.02000046,"u":"Hz","d":2},"Temperature":{"v":11.39999962,"u":"°C","d":1},"PowerFactor":{"v":1,"u":"","d":3},"ReactivePower":{"v":0.100000001,"u":"var","d":1},"Efficiency":{"v":95.24456024,"u":"%","d":2}},"1":{"Power":{"v":36.5,"u":"W","d":1},"Voltage":{"v":39.09999847,"u":"V","d":1},"Current":{"v":0.930000007,"u":"A","d":2},"YieldDay":{"v":31,"u":"Wh","d":0},"YieldTotal":{"v":4.301000118,"u":"kWh","d":3},"Irradiation":{"v":8.902439117,"u":"%","d":2}},"2":{"Power":{"v":37.09999847,"u":"W","d":1},"Voltage":{"v":40.79999924,"u":"V","d":1},"Current":{"v":0.910000026,"u":"A","d":2},"YieldDay":{"v":17,"u":"Wh","d":0},"YieldTotal":{"v":44.23899841,"u":"kWh","d":3},"Irradiation":{"v":9.048780441,"u":"%","d":2}},"events":1}],"total":{"Power":{"v":140.7999878,"u":"W","d":1},"YieldDay":{"v":71,"u":"Wh","d":0},"YieldTotal":{"v":199.0449982,"u":"kWh","d":2}}} +``` + + +To enhance readability (and filter information) use the JSON command line processor `jq`. + +``` +~$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq +{ + "inverters": [ + { + "serial": "11418186xxxx", + "name": "HM600", + "data_age": 4, + "reachable": true, + "producing": true, + "limit_relative": 100, + "limit_absolute": 600, + "0": { + "Power": { + "v": 70.69999695, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 233, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0.300000012, + "u": "A", + "d": 2 + }, + "Power DC": { + "v": 74, + "u": "W", + "d": 2 + }, + "YieldDay": { + "v": 23, + "u": "Wh", + "d": 2 + }, + "YieldTotal": { + "v": 150.5050049, + "u": "kWh", + "d": 2 + }, + "Frequency": { + "v": 50.02000046, + "u": "Hz", + "d": 2 + }, + "Temperature": { + "v": 8.300000191, + "u": "°C", + "d": 1 + }, + "PowerFactor": { + "v": 1, + "u": "", + "d": 3 + }, + "ReactivePower": { + "v": 0.100000001, + "u": "var", + "d": 1 + }, + "Efficiency": { + "v": 95.54053497, + "u": "%", + "d": 2 + } + }, + "1": { + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 1, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0.02, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 49.0320015, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 2 + } + }, + "2": { + "Power": { + "v": 74, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 42.40000153, + "u": "V", + "d": 1 + }, + "Current": { + "v": 1.74000001, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 23, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 101.4729996, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 18.04878044, + "u": "%", + "d": 2 + } + }, + "events": 3 + }, + { + "serial": "11418180xxxx", + "name": "HM800", + "data_age": 11, + "reachable": true, + "producing": true, + "limit_relative": 100, + "limit_absolute": 800, + "0": { + "Power": { + "v": 70.09999847, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 233.1000061, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0.300000012, + "u": "A", + "d": 2 + }, + "Power DC": { + "v": 73.59999847, + "u": "W", + "d": 2 + }, + "YieldDay": { + "v": 48, + "u": "Wh", + "d": 2 + }, + "YieldTotal": { + "v": 48.5399971, + "u": "kWh", + "d": 2 + }, + "Frequency": { + "v": 50.02000046, + "u": "Hz", + "d": 2 + }, + "Temperature": { + "v": 11.39999962, + "u": "°C", + "d": 1 + }, + "PowerFactor": { + "v": 1, + "u": "", + "d": 3 + }, + "ReactivePower": { + "v": 0.100000001, + "u": "var", + "d": 1 + }, + "Efficiency": { + "v": 95.24456024, + "u": "%", + "d": 2 + } + }, + "1": { + "Power": { + "v": 36.5, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 39.09999847, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0.930000007, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 31, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 4.301000118, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 8.902439117, + "u": "%", + "d": 2 + } + }, + "2": { + "Power": { + "v": 37.09999847, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 40.79999924, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0.910000026, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 17, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 44.23899841, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 9.048780441, + "u": "%", + "d": 2 + } + }, + "events": 1 + } + ], + "total": { + "Power": { + "v": 140.7999878, + "u": "W", + "d": 1 + }, + "YieldDay": { + "v": 71, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 199.0449982, + "u": "kWh", + "d": 2 + } + } +} +``` + +The eventlog can be fetched with the inverter serial number as parameter: + +``` +martin@bln9716cm ~/swbuild/OpenDTU $ curl --no-progress-meter http://192.168.10.10/api/eventlog/status?inv=11418186xxxx | jq +{ + "11418186xxxx": { + "count": 4, + "events": [ + { + "message_id": 1, + "message": "Inverter start", + "start_time": 28028, + "end_time": 28028 + }, + { + "message_id": 209, + "message": "PV-1: No input", + "start_time": 28036, + "end_time": 0 + }, + { + "message_id": 2, + "message": "DTU command failed", + "start_time": 28092, + "end_time": 28092 + }, + { + "message_id": 207, + "message": "MPPT-A: Input undervoltage", + "start_time": 28336, + "end_time": 0 + } + ] + } +} +``` + +#### combine curl and jq + +`jq` can filter specific fields from json output. + +For example, filter out the current total power: +``` +~$ curl --no-progress-meter http://192.168.10.10/api/livedata/status | jq '.total | .Power.v' +140.7999878 +``` + +#### Get information where login is required + +When config data is requested, username and password have to be provided to `curl` +Username is always `admin`, the default password is `openDTU42`. The password is used for both the admin login and the Admin-mode Access Point. + +``` +~$ curl --u admin:openDTU42 http://192.168.10.10/api/ntp/config +{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"} +``` + +### Post information + +With HTTP POST commands information can be written to the OpenDTU. + +The Web API is designed to allow the web frontend in the web browser to communicate with the OpenDTU software running on the ESP32. It is not designed to be intuitive or user-friendly, so please follow the instructions here. + +#### Example 1: change ntp settings + +If you want to configure the ntp server setting, first fetch the information from the web API: + +``` +~$ curl -u "admin:password" http://192.168.10.10/api/ntp/config +{"ntp_server":"pool.ntp.org","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"} +``` + +Then, second step, send your new settings. Use the text output from curl in the first step, add `data=` and enclose the whole data with single quotes. + +``` +~$ curl -u "admin:password" http://192.168.10.10/api/ntp/config -d 'data={"ntp_server":"my.own.ntp.server.home","ntp_timezone":"CET-1CEST,M3.5.0,M10.5.0/3","ntp_timezone_descr":"Europe/Berlin"}' +{"type":"success","message":"Settings saved!"} +``` +You will receive a json formatted response. + +#### Example 2: change power limit + +In the second example, I want to change the non persistent power limit of an inverter. Again, first fetch current data: + +``` +~$ curl http://192.168.10.10/api/limit/status +{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Ok"}} +``` + +I see data from two configured inverters. + +Now I set the relative power limit of inverter with serialnumber `11418180xxxx` to 50%. + +``` +~$ curl -u "admin:password" http://192.168.10.10/api/limit/config -d 'data={"serial":"11418180xxxx", "limit_type":1, "limit_value":50}' +{"type":"success","message":"Settings saved!"} +``` + +Then I read again the limit status. In the first answer the status is `pending`, some seconds later it changed to `OK`. + +``` +~$ curl http://192.168.10.10/api/limit/status +{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":100,"max_power":800,"limit_set_status":"Pending"}} + +... + +~$ curl http://192.168.10.10/api/limit/status +{"11418186xxxx":{"limit_relative":100,"max_power":600,"limit_set_status":"Ok"},"11418180xxxx":{"limit_relative":50,"max_power":800,"limit_set_status":"Ok"}} +``` diff --git a/include/WebApi.h b/include/WebApi.h index 44bed9b8..32b15b01 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_prometheus.h" #include "WebApi_security.h" #include "WebApi_sysstatus.h" #include "WebApi_webapp.h" @@ -26,6 +27,8 @@ public: void init(); void loop(); + static bool checkCredentials(AsyncWebServerRequest* request); + private: AsyncWebServer _server; AsyncEventSource _events; @@ -41,6 +44,7 @@ private: WebApiNetworkClass _webApiNetwork; WebApiNtpClass _webApiNtp; WebApiPowerClass _webApiPower; + WebApiPrometheusClass _webApiPrometheus; WebApiSecurityClass _webApiSecurity; WebApiSysstatusClass _webApiSysstatus; WebApiWebappClass _webApiWebapp; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h new file mode 100644 index 00000000..87a406e3 --- /dev/null +++ b/include/WebApi_prometheus.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Hoymiles.h" +#include + +class WebApiPrometheusClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onPrometheusMetricsGet(AsyncWebServerRequest* request); + + void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, const char* channelName = NULL); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_security.h b/include/WebApi_security.h index d94a7eeb..ea9dd75c 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -12,5 +12,7 @@ private: void onPasswordGet(AsyncWebServerRequest* request); void onPasswordPost(AsyncWebServerRequest* request); + void onAuthenticateGet(AsyncWebServerRequest* request); + AsyncWebServer* _server; }; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index d7c18618..dfefa7b2 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -9,6 +9,7 @@ #define ACCESS_POINT_NAME "OpenDTU-" #define ACCESS_POINT_PASSWORD "openDTU42" +#define AUTH_USERNAME "admin" #define ADMIN_TIMEOUT 180 #define WIFI_RECONNECT_TIMEOUT 15 diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 6107789d..355a4db2 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -4,10 +4,16 @@ #include "inverters/HM_4CH.h" #include +#define HOY_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY) +#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) + HoymilesClass Hoymiles; void HoymilesClass::init() { + _xSemaphore = xSemaphoreCreateMutex(); + HOY_SEMAPHORE_GIVE(); // release before first use + _pollInterval = 0; _radio.reset(new HoymilesRadio()); _radio->init(); @@ -15,49 +21,51 @@ void HoymilesClass::init() void HoymilesClass::loop() { + HOY_SEMAPHORE_TAKE(); _radio->loop(); if (getNumInverters() > 0) { if (millis() - _lastPoll > (_pollInterval * 1000)) { static uint8_t inverterPos = 0; - std::shared_ptr iv = getInverterByPos(inverterPos); - if (iv != nullptr && _radio->isIdle()) { - Serial.print(F("Fetch inverter: ")); - Serial.println(iv->serial(), HEX); + if (_radio->isIdle()) { + std::shared_ptr iv = getInverterByPos(inverterPos); + if (iv != nullptr) { + Serial.print(F("Fetch inverter: ")); + Serial.println(iv->serial(), HEX); - iv->sendStatsRequest(_radio.get()); + iv->sendStatsRequest(_radio.get()); - // Fetch event log - bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(_radio.get(), force); + // Fetch event log + bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; + iv->sendAlarmLogRequest(_radio.get(), force); - // Fetch limit - if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) - || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) - && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - Serial.println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(_radio.get()); + // Fetch limit + if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) + || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) + && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { + Serial.println("Request SystemConfigPara"); + iv->sendSystemConfigParaRequest(_radio.get()); + } + + // Set limit if required + if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { + Serial.println(F("Resend ActivePowerControl")); + iv->resendActivePowerControlRequest(_radio.get()); + } + + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + Serial.println(F("Resend PowerCommand")); + iv->resendPowerControlRequest(_radio.get()); + } + + // Fetch dev info (but first fetch stats) + if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { + Serial.println(F("Request device info")); + iv->sendDevInfoRequest(_radio.get()); + } } - - // Set limit if required - if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - Serial.println(F("Resend ActivePowerControl")); - iv->resendActivePowerControlRequest(_radio.get()); - } - - // Set power status if required - if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - Serial.println(F("Resend PowerCommand")); - iv->resendPowerControlRequest(_radio.get()); - } - - // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { - Serial.println(F("Request device info")); - iv->sendDevInfoRequest(_radio.get()); - } - if (++inverterPos >= getNumInverters()) { inverterPos = 0; } @@ -66,6 +74,8 @@ void HoymilesClass::loop() _lastPoll = millis(); } } + + HOY_SEMAPHORE_GIVE(); } std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) @@ -135,7 +145,9 @@ void HoymilesClass::removeInverterBySerial(uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { + HOY_SEMAPHORE_TAKE(); _inverters.erase(_inverters.begin() + i); + HOY_SEMAPHORE_GIVE(); return; } } diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index 072f1965..4b6d37d6 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -31,6 +31,8 @@ private: std::vector> _inverters; std::unique_ptr _radio; + SemaphoreHandle_t _xSemaphore; + uint32_t _pollInterval = 0; uint32_t _lastPoll = 0; }; diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index 8cbc6e10..db70d8b5 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -127,7 +127,7 @@ void HoymilesRadio::loop() } } else { // If inverter was not found, assume the command is invalid - Serial.println(F("Invalid inverter found")); + Serial.println(F("RX: Invalid inverter found")); _commandQueue.pop(); _busyFlag = false; } @@ -137,8 +137,13 @@ void HoymilesRadio::loop() CommandAbstract* cmd = _commandQueue.front().get(); auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); - inv->clearRxFragmentBuffer(); - sendEsbPacket(cmd); + if (nullptr != inv) { + inv->clearRxFragmentBuffer(); + sendEsbPacket(cmd); + } else { + Serial.println(F("TX: Invalid inverter found")); + _commandQueue.pop(); + } } } } diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 5fc0bbb2..36b25af3 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -14,7 +14,7 @@ HM_Abstract::HM_Abstract(uint64_t serial) bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) { struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { return false; } @@ -31,7 +31,7 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) { struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { return false; } @@ -59,7 +59,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) { struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { return false; } @@ -80,7 +80,7 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) { struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { return false; } diff --git a/platformio.ini b/platformio.ini index b91ddf79..7295d4ab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,7 +23,7 @@ build_flags = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer bblanchon/ArduinoJson @ ^6.19.4 - https://github.com/bertmelis/espMqttClient.git#v1.3.2 + https://github.com/bertmelis/espMqttClient.git#v1.3.3 nrf24/RF24 @ ^1.4.5 extra_scripts = diff --git a/src/MqttHassPublishing.cpp b/src/MqttHassPublishing.cpp index 60705b4a..d103281e 100644 --- a/src/MqttHassPublishing.cpp +++ b/src/MqttHassPublishing.cpp @@ -152,7 +152,7 @@ void MqttHassPublishingClass::publishInverterButton(std::shared_ptrname()) + " " + caption; root[F("uniq_id")] = serial + "_" + buttonId; if (strcmp(icon, "")) { root[F("ic")] = icon; @@ -191,7 +191,7 @@ void MqttHassPublishingClass::publishInverterNumber( String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; DynamicJsonDocument root(1024); - root[F("name")] = caption; + root[F("name")] = String(inv->name()) + " " + caption; root[F("uniq_id")] = serial + "_" + buttonId; if (strcmp(icon, "")) { root[F("ic")] = icon; @@ -226,7 +226,7 @@ void MqttHassPublishingClass::publishInverterBinarySensor(std::shared_ptrname()) + " " + caption; root[F("uniq_id")] = serial + "_" + sensorId; root[F("stat_t")] = statTopic; root[F("pl_on")] = payload_on; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index dcc60dff..8000be9c 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -5,6 +5,7 @@ #include "WebApi.h" #include "ArduinoJson.h" #include "AsyncJson.h" +#include "Configuration.h" #include "defaults.h" WebApiClass::WebApiClass() @@ -28,6 +29,7 @@ void WebApiClass::init() _webApiNetwork.init(&_server); _webApiNtp.init(&_server); _webApiPower.init(&_server); + _webApiPrometheus.init(&_server); _webApiSecurity.init(&_server); _webApiSysstatus.init(&_server); _webApiWebapp.init(&_server); @@ -59,4 +61,22 @@ void WebApiClass::loop() _webApiVedirect.loop(); } +bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) +{ + CONFIG_T& config = Configuration.get(); + if (request->authenticate(AUTH_USERNAME, config.Security_Password)) { + return true; + } + + AsyncWebServerResponse* r = request->beginResponse(401); + + // WebAPI should set the X-Requested-With to prevent browser internal auth dialogs + if (!request->hasHeader("X-Requested-With")) { + r->addHeader(F("WWW-Authenticate"), F("Basic realm=\"Login Required\"")); + } + request->send(r); + + return false; +} + WebApiClass WebApi; \ No newline at end of file diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index e4376432..fe522652 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" +#include "WebApi.h" #include void WebApiConfigClass::init(AsyncWebServer* server) @@ -32,11 +33,19 @@ void WebApiConfigClass::loop() void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true); } void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); @@ -93,6 +102,10 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + // the request handler is triggered after the upload has finished... // create the response, add header, and send response @@ -108,6 +121,10 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { + if (!WebApi.checkCredentials(request)) { + return; + } + 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"); diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index b832c26d..bb61b059 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "Hoymiles.h" +#include "WebApi.h" void WebApiDtuClass::init(AsyncWebServer* server) { @@ -24,6 +25,10 @@ void WebApiDtuClass::loop() void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -43,6 +48,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 9b33dc77..48984f42 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "Update.h" +#include "WebApi.h" #include "helper.h" void WebApiFirmwareClass::init(AsyncWebServer* server) @@ -31,6 +32,10 @@ void WebApiFirmwareClass::loop() void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + // the request handler is triggered after the upload has finished... // create the response, add header, and send response @@ -46,6 +51,10 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { + if (!WebApi.checkCredentials(request)) { + return; + } + // Upload handler chunks in data if (!index) { if (!request->hasParam("MD5", true)) { diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 12cadaf0..2fb254f9 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -8,6 +8,7 @@ #include "Configuration.h" #include "Hoymiles.h" #include "MqttHassPublishing.h" +#include "WebApi.h" #include "helper.h" void WebApiInverterClass::init(AsyncWebServer* server) @@ -28,6 +29,10 @@ void WebApiInverterClass::loop() void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U); JsonObject root = response->getRoot(); JsonArray data = root.createNestedArray(F("inverter")); @@ -48,13 +53,16 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj[F("serial")] = buffer; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); + uint8_t max_channels; if (inv == nullptr) { obj[F("type")] = F("Unknown"); + max_channels = INV_MAX_CHAN_COUNT; } else { obj[F("type")] = inv->typeName(); + max_channels = inv->Statistics()->getChannelCount(); } - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + for (uint8_t c = 0; c < max_channels; c++) { obj[F("max_power")][c] = config.Inverter[i].MaxChannelPower[c]; } } @@ -66,6 +74,10 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); @@ -151,6 +163,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); @@ -210,7 +226,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } JsonArray maxPowerArray = root[F("max_power")].as(); - if (maxPowerArray.size() != INV_MAX_CHAN_COUNT) { + if (maxPowerArray.size() == 0 || maxPowerArray.size() > INV_MAX_CHAN_COUNT) { retMsg[F("message")] = F("Invalid amount of max channel setting given!"); response->setLength(); request->send(response); @@ -265,6 +281,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 33bcd407..00880c31 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Hoymiles.h" +#include "WebApi.h" void WebApiLimitClass::init(AsyncWebServer* server) { @@ -54,6 +55,10 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 78cbbd0b..3083851e 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -8,6 +8,7 @@ #include "Configuration.h" #include "MqttHassPublishing.h" #include "MqttSettings.h" +#include "WebApi.h" #include "helper.h" void WebApiMqttClass::init(AsyncWebServer* server) @@ -54,6 +55,10 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminGet(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(); @@ -83,6 +88,10 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminPost(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"); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 27760f9d..4c3cf2b4 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "NetworkSettings.h" +#include "WebApi.h" #include "helper.h" void WebApiNetworkClass::init(AsyncWebServer* server) @@ -52,6 +53,10 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -72,6 +77,10 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 5f2bae71..b019d239 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -7,6 +7,7 @@ #include "AsyncJson.h" #include "Configuration.h" #include "NtpSettings.h" +#include "WebApi.h" #include "helper.h" void WebApiNtpClass::init(AsyncWebServer* server) @@ -37,7 +38,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { root[F("ntp_status")] = false; } else { root[F("ntp_status")] = true; @@ -52,6 +53,10 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -66,6 +71,10 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); @@ -142,11 +151,15 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); struct tm timeinfo; - if (!getLocalTime(&timeinfo, 0)) { + if (!getLocalTime(&timeinfo, 5)) { root[F("ntp_status")] = false; } else { root[F("ntp_status")] = true; @@ -165,6 +178,10 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 8e3c2bc8..413bc0fd 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Hoymiles.h" +#include "WebApi.h" void WebApiPowerClass::init(AsyncWebServer* server) { @@ -47,6 +48,10 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp new file mode 100644 index 00000000..e5346e2d --- /dev/null +++ b/src/WebApi_prometheus.cpp @@ -0,0 +1,97 @@ +#include "WebApi_prometheus.h" + +#include "Configuration.h" +#include "Hoymiles.h" +#include "NetworkSettings.h" + +void WebApiPrometheusClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); +} + +void WebApiPrometheusClass::loop() +{ +} + +void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* request) +{ + auto stream = request->beginResponseStream("text/plain; charset=utf-8", 40960); + + stream->print(F("# HELP opendtu_build Build info\n")); + stream->print(F("# TYPE opendtu_build gauge\n")); + stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", + NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); + + stream->print(F("# HELP opendtu_platform Platform info\n")); + stream->print(F("# TYPE opendtu_platform gauge\n")); + stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), WiFi.macAddress().c_str()); + + stream->print(F("# HELP opendtu_uptime Uptime in seconds\n")); + stream->print(F("# TYPE opendtu_uptime counter\n")); + stream->printf("opendtu_uptime %lld\n", esp_timer_get_time() / 1000000); + + stream->print(F("# HELP opendtu_heap_size System memory size\n")); + stream->print(F("# TYPE opendtu_heap_size gauge\n")); + stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); + + stream->print(F("# HELP opendtu_free_heap_size System free memory\n")); + stream->print(F("# TYPE opendtu_free_heap_size gauge\n")); + stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); + + stream->print(F("# HELP wifi_rssi WiFi RSSI\n")); + stream->print(F("# TYPE wifi_rssi gauge\n")); + stream->printf("wifi_rssi %d\n", WiFi.RSSI()); + + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String serial = inv->serialString(); + const char* name = inv->name(); + if (i == 0) { + stream->print(F("# HELP opendtu_last_update last update from inverter in s\n")); + stream->print(F("# TYPE opendtu_last_update gauge\n")); + } + stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", + serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); + + // Loop all channels + for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { + addField(stream, serial, i, inv, c, FLD_PAC); + addField(stream, serial, i, inv, c, FLD_UAC); + addField(stream, serial, i, inv, c, FLD_IAC); + if (c == 0) { + addField(stream, serial, i, inv, c, FLD_PDC, "PowerDC"); + } else { + addField(stream, serial, i, inv, c, FLD_PDC); + } + addField(stream, serial, i, inv, c, FLD_UDC); + addField(stream, serial, i, inv, c, FLD_IDC); + addField(stream, serial, i, inv, c, FLD_YD); + addField(stream, serial, i, inv, c, FLD_YT); + addField(stream, serial, i, inv, c, FLD_F); + addField(stream, serial, i, inv, c, FLD_T); + addField(stream, serial, i, inv, c, FLD_PF); + addField(stream, serial, i, inv, c, FLD_PRA); + addField(stream, serial, i, inv, c, FLD_EFF); + addField(stream, serial, i, inv, c, FLD_IRR); + } + } + stream->addHeader(F("Cache-Control"), F("no-cache")); + request->send(stream); +} + +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, const char* channelName) +{ + if (inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { + const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(channel, fieldId) : channelName; + if (idx == 0 && channel == 0) { + stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(channel, fieldId)); + stream->printf("# TYPE opendtu_%s gauge\n", chanName); + } + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", chanName, serial.c_str(), idx, inv->name(), channel, inv->Statistics()->getChannelFieldValue(channel, fieldId)); + } +} \ No newline at end of file diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index 2009be96..96802fbc 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -6,6 +6,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" +#include "WebApi.h" #include "helper.h" void WebApiSecurityClass::init(AsyncWebServer* server) @@ -16,6 +17,7 @@ void WebApiSecurityClass::init(AsyncWebServer* 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)); + _server->on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); } void WebApiSecurityClass::loop() @@ -24,6 +26,10 @@ void WebApiSecurityClass::loop() void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -36,6 +42,10 @@ void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonObject retMsg = response->getRoot(); retMsg[F("type")] = F("warning"); @@ -87,6 +97,21 @@ void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); + response->setLength(); + request->send(response); +} + +void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Authentication successfull!"); + response->setLength(); request->send(response); } \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index 449fb09b..cdbe1756 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -14,23 +14,25 @@ "@popperjs/core": "^2.11.6", "bootstrap": "^5.2.2", "bootstrap-icons-vue": "^1.8.1", + "mitt": "^3.0.0", "spark-md5": "^3.0.2", - "vue": "^3.2.41", + "vue": "^3.2.45", "vue-router": "^4.1.6" }, "devDependencies": { "@rushstack/eslint-patch": "^1.2.0", - "@types/bootstrap": "^5.2.5", - "@types/node": "^18.11.8", + "@types/bootstrap": "^5.2.6", + "@types/node": "^18.11.9", "@types/spark-md5": "^3.0.2", "@vitejs/plugin-vue": "^3.2.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", - "eslint": "^8.26.0", + "eslint": "^8.27.0", "eslint-plugin-vue": "^9.7.0", "npm-run-all": "^4.1.5", + "sass": "^1.56.1", "typescript": "^4.8.4", - "vite": "^3.2.2", + "vite": "^3.2.3", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^2.1.1", "vue-tsc": "^1.0.9" diff --git a/webapp/src/components/LimitSettingsCurrent.vue b/webapp/src/components/LimitSettingsCurrent.vue index 24ced637..617d106c 100644 --- a/webapp/src/components/LimitSettingsCurrent.vue +++ b/webapp/src/components/LimitSettingsCurrent.vue @@ -14,7 +14,7 @@ import { defineComponent } from 'vue'; import { formatNumber } from '@/utils'; declare interface LimitData { - limit: number, + limit: number; } export default defineComponent({ diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 28582f92..793bb420 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -7,7 +7,7 @@ @@ -85,13 +89,39 @@ \ No newline at end of file diff --git a/webapp/src/views/LoginView.vue b/webapp/src/views/LoginView.vue new file mode 100644 index 00000000..c5d43e09 --- /dev/null +++ b/webapp/src/views/LoginView.vue @@ -0,0 +1,89 @@ + + + \ No newline at end of file diff --git a/webapp/src/views/MqttAdminView.vue b/webapp/src/views/MqttAdminView.vue index 2ada9558..b2f62d96 100644 --- a/webapp/src/views/MqttAdminView.vue +++ b/webapp/src/views/MqttAdminView.vue @@ -218,6 +218,7 @@ import { defineComponent } from 'vue'; import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; +import { handleResponse, authHeader } from '@/utils/authentication'; import type { MqttConfig } from "@/types/MqttConfig"; export default defineComponent({ @@ -240,8 +241,8 @@ export default defineComponent({ methods: { getMqttConfig() { this.dataLoading = true; - fetch("/api/mqtt/config") - .then((response) => response.json()) + fetch("/api/mqtt/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then((data) => { this.mqttConfigList = data; this.dataLoading = false; @@ -255,15 +256,10 @@ export default defineComponent({ fetch("/api/mqtt/config", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; diff --git a/webapp/src/views/NetworkAdminView.vue b/webapp/src/views/NetworkAdminView.vue index a9c0b537..235869ce 100644 --- a/webapp/src/views/NetworkAdminView.vue +++ b/webapp/src/views/NetworkAdminView.vue @@ -104,6 +104,7 @@ import { defineComponent } from 'vue'; import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; +import { handleResponse, authHeader } from '@/utils/authentication'; import type { NetworkConfig } from "@/types/NetworkkConfig"; export default defineComponent({ @@ -126,8 +127,8 @@ export default defineComponent({ methods: { getNetworkConfig() { this.dataLoading = true; - fetch("/api/network/config") - .then((response) => response.json()) + fetch("/api/network/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then((data) => { this.networkConfigList = data; this.dataLoading = false; @@ -141,15 +142,10 @@ export default defineComponent({ fetch("/api/network/config", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; diff --git a/webapp/src/views/NtpAdminView.vue b/webapp/src/views/NtpAdminView.vue index 138edc9b..be6a13e3 100644 --- a/webapp/src/views/NtpAdminView.vue +++ b/webapp/src/views/NtpAdminView.vue @@ -75,6 +75,7 @@ import { defineComponent } from 'vue'; import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; +import { handleResponse, authHeader } from '@/utils/authentication'; import type { NtpConfig } from "@/types/NtpConfig"; export default defineComponent({ @@ -127,8 +128,8 @@ export default defineComponent({ }, getNtpConfig() { this.dataLoading = true; - fetch("/api/ntp/config") - .then((response) => response.json()) + fetch("/api/ntp/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then( (data) => { this.ntpConfigList = data; @@ -142,8 +143,8 @@ export default defineComponent({ }, getCurrentTime() { this.dataLoading = true; - fetch("/api/ntp/time") - .then((response) => response.json()) + fetch("/api/ntp/time", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then( (data) => { this.mcuTime = new Date( @@ -168,15 +169,10 @@ export default defineComponent({ fetch("/api/ntp/time", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; @@ -196,15 +192,10 @@ export default defineComponent({ fetch("/api/ntp/config", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; diff --git a/webapp/src/views/SecurityAdminView.vue b/webapp/src/views/SecurityAdminView.vue index f6878172..b4aae349 100644 --- a/webapp/src/views/SecurityAdminView.vue +++ b/webapp/src/views/SecurityAdminView.vue @@ -26,8 +26,8 @@ @@ -41,6 +41,7 @@ import { defineComponent } from 'vue'; import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; +import { handleResponse, authHeader } from '@/utils/authentication'; import type { SecurityConfig } from '@/types/SecurityConfig'; export default defineComponent({ @@ -65,8 +66,8 @@ export default defineComponent({ methods: { getPasswordConfig() { this.dataLoading = true; - fetch("/api/security/password") - .then((response) => response.json()) + fetch("/api/security/password", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then( (data) => { this.securityConfigList = data; @@ -90,15 +91,10 @@ export default defineComponent({ fetch("/api/security/password", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; diff --git a/webapp/src/views/VedirectAdminView.vue b/webapp/src/views/VedirectAdminView.vue index 33ba3e33..3a313b69 100644 --- a/webapp/src/views/VedirectAdminView.vue +++ b/webapp/src/views/VedirectAdminView.vue @@ -51,6 +51,7 @@ import { defineComponent } from 'vue'; import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; +import { handleResponse, authHeader } from '@/utils/authentication'; import type { VedirectConfig } from "@/types/VedirectConfig"; export default defineComponent({ @@ -73,8 +74,8 @@ export default defineComponent({ methods: { getVedirectConfig() { this.dataLoading = true; - fetch("/api/vedirect/config") - .then((response) => response.json()) + fetch("api/vedirect/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter)) .then((data) => { this.vedirectConfigList = data; this.dataLoading = false; @@ -88,15 +89,10 @@ export default defineComponent({ fetch("/api/vedirect/config", { method: "POST", + headers: authHeader(), body: formData, }) - .then(function (response) { - if (response.status != 200) { - throw response.status; - } else { - return response.json(); - } - }) + .then((response) => handleResponse(response, this.$emitter)) .then( (response) => { this.alertMessage = response.message; diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 7e7f3f93..ca362f7b 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -6,6 +6,8 @@ import vue from '@vitejs/plugin-vue' import viteCompression from 'vite-plugin-compression'; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' +const path = require('path') + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -14,7 +16,8 @@ export default defineConfig({ cssInjectedByJsPlugin()], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) + '@': fileURLToPath(new URL('./src', import.meta.url)), + '~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'), } }, build: { diff --git a/webapp/yarn.lock b/webapp/yarn.lock index fd33b4f2..17206ddd 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -87,10 +87,10 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== -"@types/bootstrap@^5.2.5": - version "5.2.5" - resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.5.tgz#0bb5dea7720611b2bb7ba16bd8a64fafd86fb658" - integrity sha512-VnalUJ3E/oaV3DYrauEc/sSPpaEPxTV09twSEzY4KFRvyuGlrZUSqG95XZ6ReAi0YMZIs7rXxdngDK2X1YONQA== +"@types/bootstrap@^5.2.6": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.6.tgz#e861b3aa1f4a1434da0bf50fbaa372b6f7e64d2f" + integrity sha512-BlAc3YATdasbHoxMoBWODrSF6qwQO/E9X8wVxCCSa6rWjnaZfpkr2N6pUMCY6jj2+wf0muUtLySbvU9etX6YqA== dependencies: "@popperjs/core" "^2.9.2" @@ -99,10 +99,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/node@^18.11.8": - version "18.11.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265" - integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A== +"@types/node@^18.11.9": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== "@types/spark-md5@^3.0.2": version "3.0.2" @@ -283,6 +283,16 @@ estree-walker "^2.0.2" source-map "^0.6.1" +"@vue/compiler-core@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b" + integrity sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/shared" "3.2.45" + estree-walker "^2.0.2" + source-map "^0.6.1" + "@vue/compiler-dom@3.2.41", "@vue/compiler-dom@^3.2.40": version "3.2.41" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz#dc63dcd3ce8ca8a8721f14009d498a7a54380299" @@ -291,7 +301,31 @@ "@vue/compiler-core" "3.2.41" "@vue/shared" "3.2.41" -"@vue/compiler-sfc@3.2.41", "@vue/compiler-sfc@^3.2.40": +"@vue/compiler-dom@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz#c43cc15e50da62ecc16a42f2622d25dc5fd97dce" + integrity sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw== + dependencies: + "@vue/compiler-core" "3.2.45" + "@vue/shared" "3.2.45" + +"@vue/compiler-sfc@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz#7f7989cc04ec9e7c55acd406827a2c4e96872c70" + integrity sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.45" + "@vue/compiler-dom" "3.2.45" + "@vue/compiler-ssr" "3.2.45" + "@vue/reactivity-transform" "3.2.45" + "@vue/shared" "3.2.45" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + +"@vue/compiler-sfc@^3.2.40": version "3.2.41" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz#238fb8c48318408c856748f4116aff8cc1dc2a73" integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w== @@ -315,6 +349,14 @@ "@vue/compiler-dom" "3.2.41" "@vue/shared" "3.2.41" +"@vue/compiler-ssr@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz#bd20604b6e64ea15344d5b6278c4141191c983b2" + integrity sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ== + dependencies: + "@vue/compiler-dom" "3.2.45" + "@vue/shared" "3.2.45" + "@vue/devtools-api@^6.4.5": version "6.4.5" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380" @@ -340,43 +382,66 @@ estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity@3.2.41", "@vue/reactivity@^3.2.40": +"@vue/reactivity-transform@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz#07ac83b8138550c83dfb50db43cde1e0e5e8124d" + integrity sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.45" + "@vue/shared" "3.2.45" + estree-walker "^2.0.2" + magic-string "^0.25.7" + +"@vue/reactivity@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.45.tgz#412a45b574de601be5a4a5d9a8cbd4dee4662ff0" + integrity sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A== + dependencies: + "@vue/shared" "3.2.45" + +"@vue/reactivity@^3.2.40": version "3.2.41" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.41.tgz#0ad3bdf76d76822da1502dc9f394dafd02642963" integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g== dependencies: "@vue/shared" "3.2.41" -"@vue/runtime-core@3.2.41": - version "3.2.41" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.41.tgz#775bfc00b3fadbaddab77138f23322aee3517a76" - integrity sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ== +"@vue/runtime-core@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz#7ad7ef9b2519d41062a30c6fa001ec43ac549c7f" + integrity sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A== dependencies: - "@vue/reactivity" "3.2.41" - "@vue/shared" "3.2.41" + "@vue/reactivity" "3.2.45" + "@vue/shared" "3.2.45" -"@vue/runtime-dom@3.2.41": - version "3.2.41" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz#cdf86be7410f7b15c29632a96ce879e5b4c9ab92" - integrity sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA== +"@vue/runtime-dom@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz#1a2ef6ee2ad876206fbbe2a884554bba2d0faf59" + integrity sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA== dependencies: - "@vue/runtime-core" "3.2.41" - "@vue/shared" "3.2.41" + "@vue/runtime-core" "3.2.45" + "@vue/shared" "3.2.45" csstype "^2.6.8" -"@vue/server-renderer@3.2.41": - version "3.2.41" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.41.tgz#ca64552c05878f94e8d191ac439141c06c0fb2ad" - integrity sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig== +"@vue/server-renderer@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz#ca9306a0c12b0530a1a250e44f4a0abac6b81f3f" + integrity sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g== dependencies: - "@vue/compiler-ssr" "3.2.41" - "@vue/shared" "3.2.41" + "@vue/compiler-ssr" "3.2.45" + "@vue/shared" "3.2.45" "@vue/shared@3.2.41", "@vue/shared@^3.2.40": version "3.2.41" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.41.tgz#fbc95422df654ea64e8428eced96ba6ad555d2bb" integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw== +"@vue/shared@3.2.45": + version "3.2.45" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2" + integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg== + "@vue/tsconfig@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.1.3.tgz#4a61dbd29783d01ddab504276dcf0c2b6988654f" @@ -421,6 +486,14 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -436,6 +509,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -466,7 +544,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -503,6 +581,21 @@ chalk@^4.0.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +"chokidar@>=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -831,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.26.0: - version "8.26.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.26.0.tgz#2bcc8836e6c424c4ac26a5674a70d44d84f2181d" - integrity sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg== +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== dependencies: "@eslint/eslintrc" "^1.3.3" "@humanwhocodes/config-array" "^0.11.6" @@ -1057,7 +1150,7 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -1168,6 +1261,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +immutable@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" + integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1215,6 +1313,13 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -1247,7 +1352,7 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1434,6 +1539,11 @@ minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" +mitt@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd" + integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -1469,6 +1579,11 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + npm-run-all@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" @@ -1601,7 +1716,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -1666,6 +1781,13 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -1729,6 +1851,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +sass@^1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" + integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -1784,7 +1915,7 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -1983,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.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.2.tgz#280762bfaf47bcea1d12698427331c0009ac7c1f" - integrity sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw== +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== dependencies: esbuild "^0.15.9" postcss "^8.4.18" @@ -2031,16 +2162,16 @@ vue-tsc@^1.0.9: "@volar/vue-language-core" "1.0.9" "@volar/vue-typescript" "1.0.9" -vue@^3.2.41: - version "3.2.41" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.41.tgz#ed452b8a0f7f2b962f055c8955139c28b1c06806" - integrity sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ== +vue@^3.2.45: + version "3.2.45" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8" + integrity sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA== dependencies: - "@vue/compiler-dom" "3.2.41" - "@vue/compiler-sfc" "3.2.41" - "@vue/runtime-dom" "3.2.41" - "@vue/server-renderer" "3.2.41" - "@vue/shared" "3.2.41" + "@vue/compiler-dom" "3.2.45" + "@vue/compiler-sfc" "3.2.45" + "@vue/runtime-dom" "3.2.45" + "@vue/server-renderer" "3.2.45" + "@vue/shared" "3.2.45" which-boxed-primitive@^1.0.2: version "1.0.2" diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 7ba8f49c..2b894a57 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ