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

This commit is contained in:
helgeerbe 2022-11-16 16:39:01 +01:00
commit f35395e76f
68 changed files with 1576 additions and 535 deletions

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

@ -37,7 +37,7 @@ jobs:
- name: Get default environments - name: Get default environments
id: envs id: envs
run: | 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: outputs:
environments: ${{ steps.envs.outputs.environments }} environments: ${{ steps.envs.outputs.environments }}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"C_Cpp.clang_format_style": "WebKit"
}

View File

@ -75,6 +75,7 @@ Sends text raw data as difined in VE.Direct spec.
* Time zone support * Time zone support
* Ve.Direct interface (via web-interface, REST-api, or MQTT) * Ve.Direct interface (via web-interface, REST-api, or MQTT)
* Ethernet support * Ethernet support
* Prometheus API endpoint (/api/prometheus/metrics)
## Features for developers ## Features for developers
* The microcontroller part * The microcontroller part
@ -206,7 +207,7 @@ Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/release
## First configuration ## First configuration
* After the initial flashing of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42". * 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) * 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. * 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. * 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). * 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 ## MQTT Topic Documentation
A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md) 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 ## Available cases
* <https://www.thingiverse.com/thing:5435911> * <https://www.thingiverse.com/thing:5435911>
* <https://www.printables.com/model/293003-sol-opendtu-esp32-nrf24l01-case> * <https://www.printables.com/model/293003-sol-opendtu-esp32-nrf24l01-case>

6
docs/README.md Normal file
View File

@ -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)

460
docs/Web-API.md Normal file
View File

@ -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"}}
```

View File

@ -12,6 +12,7 @@
#include "WebApi_network.h" #include "WebApi_network.h"
#include "WebApi_ntp.h" #include "WebApi_ntp.h"
#include "WebApi_power.h" #include "WebApi_power.h"
#include "WebApi_prometheus.h"
#include "WebApi_security.h" #include "WebApi_security.h"
#include "WebApi_sysstatus.h" #include "WebApi_sysstatus.h"
#include "WebApi_webapp.h" #include "WebApi_webapp.h"
@ -26,6 +27,8 @@ public:
void init(); void init();
void loop(); void loop();
static bool checkCredentials(AsyncWebServerRequest* request);
private: private:
AsyncWebServer _server; AsyncWebServer _server;
AsyncEventSource _events; AsyncEventSource _events;
@ -41,6 +44,7 @@ private:
WebApiNetworkClass _webApiNetwork; WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp; WebApiNtpClass _webApiNtp;
WebApiPowerClass _webApiPower; WebApiPowerClass _webApiPower;
WebApiPrometheusClass _webApiPrometheus;
WebApiSecurityClass _webApiSecurity; WebApiSecurityClass _webApiSecurity;
WebApiSysstatusClass _webApiSysstatus; WebApiSysstatusClass _webApiSysstatus;
WebApiWebappClass _webApiWebapp; WebApiWebappClass _webApiWebapp;

View File

@ -0,0 +1,17 @@
#pragma once
#include "Hoymiles.h"
#include <ESPAsyncWebServer.h>
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<InverterAbstract> inv, uint8_t channel, uint8_t fieldId, const char* channelName = NULL);
AsyncWebServer* _server;
};

View File

@ -12,5 +12,7 @@ private:
void onPasswordGet(AsyncWebServerRequest* request); void onPasswordGet(AsyncWebServerRequest* request);
void onPasswordPost(AsyncWebServerRequest* request); void onPasswordPost(AsyncWebServerRequest* request);
void onAuthenticateGet(AsyncWebServerRequest* request);
AsyncWebServer* _server; AsyncWebServer* _server;
}; };

View File

@ -9,6 +9,7 @@
#define ACCESS_POINT_NAME "OpenDTU-" #define ACCESS_POINT_NAME "OpenDTU-"
#define ACCESS_POINT_PASSWORD "openDTU42" #define ACCESS_POINT_PASSWORD "openDTU42"
#define AUTH_USERNAME "admin"
#define ADMIN_TIMEOUT 180 #define ADMIN_TIMEOUT 180
#define WIFI_RECONNECT_TIMEOUT 15 #define WIFI_RECONNECT_TIMEOUT 15

View File

@ -4,10 +4,16 @@
#include "inverters/HM_4CH.h" #include "inverters/HM_4CH.h"
#include <Arduino.h> #include <Arduino.h>
#define HOY_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY)
#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore)
HoymilesClass Hoymiles; HoymilesClass Hoymiles;
void HoymilesClass::init() void HoymilesClass::init()
{ {
_xSemaphore = xSemaphoreCreateMutex();
HOY_SEMAPHORE_GIVE(); // release before first use
_pollInterval = 0; _pollInterval = 0;
_radio.reset(new HoymilesRadio()); _radio.reset(new HoymilesRadio());
_radio->init(); _radio->init();
@ -15,14 +21,16 @@ void HoymilesClass::init()
void HoymilesClass::loop() void HoymilesClass::loop()
{ {
HOY_SEMAPHORE_TAKE();
_radio->loop(); _radio->loop();
if (getNumInverters() > 0) { if (getNumInverters() > 0) {
if (millis() - _lastPoll > (_pollInterval * 1000)) { if (millis() - _lastPoll > (_pollInterval * 1000)) {
static uint8_t inverterPos = 0; static uint8_t inverterPos = 0;
if (_radio->isIdle()) {
std::shared_ptr<InverterAbstract> iv = getInverterByPos(inverterPos); std::shared_ptr<InverterAbstract> iv = getInverterByPos(inverterPos);
if (iv != nullptr && _radio->isIdle()) { if (iv != nullptr) {
Serial.print(F("Fetch inverter: ")); Serial.print(F("Fetch inverter: "));
Serial.println(iv->serial(), HEX); Serial.println(iv->serial(), HEX);
@ -57,7 +65,7 @@ void HoymilesClass::loop()
Serial.println(F("Request device info")); Serial.println(F("Request device info"));
iv->sendDevInfoRequest(_radio.get()); iv->sendDevInfoRequest(_radio.get());
} }
}
if (++inverterPos >= getNumInverters()) { if (++inverterPos >= getNumInverters()) {
inverterPos = 0; inverterPos = 0;
} }
@ -66,6 +74,8 @@ void HoymilesClass::loop()
_lastPoll = millis(); _lastPoll = millis();
} }
} }
HOY_SEMAPHORE_GIVE();
} }
std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, uint64_t serial) std::shared_ptr<InverterAbstract> 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++) { for (uint8_t i = 0; i < _inverters.size(); i++) {
if (_inverters[i]->serial() == serial) { if (_inverters[i]->serial() == serial) {
HOY_SEMAPHORE_TAKE();
_inverters.erase(_inverters.begin() + i); _inverters.erase(_inverters.begin() + i);
HOY_SEMAPHORE_GIVE();
return; return;
} }
} }

View File

@ -31,6 +31,8 @@ private:
std::vector<std::shared_ptr<InverterAbstract>> _inverters; std::vector<std::shared_ptr<InverterAbstract>> _inverters;
std::unique_ptr<HoymilesRadio> _radio; std::unique_ptr<HoymilesRadio> _radio;
SemaphoreHandle_t _xSemaphore;
uint32_t _pollInterval = 0; uint32_t _pollInterval = 0;
uint32_t _lastPoll = 0; uint32_t _lastPoll = 0;
}; };

View File

@ -127,7 +127,7 @@ void HoymilesRadio::loop()
} }
} else { } else {
// If inverter was not found, assume the command is invalid // 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(); _commandQueue.pop();
_busyFlag = false; _busyFlag = false;
} }
@ -137,8 +137,13 @@ void HoymilesRadio::loop()
CommandAbstract* cmd = _commandQueue.front().get(); CommandAbstract* cmd = _commandQueue.front().get();
auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress()); auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress());
if (nullptr != inv) {
inv->clearRxFragmentBuffer(); inv->clearRxFragmentBuffer();
sendEsbPacket(cmd); sendEsbPacket(cmd);
} else {
Serial.println(F("TX: Invalid inverter found"));
_commandQueue.pop();
}
} }
} }
} }

View File

@ -14,7 +14,7 @@ HM_Abstract::HM_Abstract(uint64_t serial)
bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio)
{ {
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
return false; return false;
} }
@ -31,7 +31,7 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio)
bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force)
{ {
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
return false; return false;
} }
@ -59,7 +59,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force)
bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
{ {
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
return false; return false;
} }
@ -80,7 +80,7 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
{ {
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
return false; return false;
} }

View File

@ -23,7 +23,7 @@ build_flags =
lib_deps = lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.19.4 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 nrf24/RF24 @ ^1.4.5
extra_scripts = extra_scripts =

View File

@ -152,7 +152,7 @@ void MqttHassPublishingClass::publishInverterButton(std::shared_ptr<InverterAbst
String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
root[F("name")] = caption; root[F("name")] = String(inv->name()) + " " + caption;
root[F("uniq_id")] = serial + "_" + buttonId; root[F("uniq_id")] = serial + "_" + buttonId;
if (strcmp(icon, "")) { if (strcmp(icon, "")) {
root[F("ic")] = icon; root[F("ic")] = icon;
@ -191,7 +191,7 @@ void MqttHassPublishingClass::publishInverterNumber(
String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic; String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
root[F("name")] = caption; root[F("name")] = String(inv->name()) + " " + caption;
root[F("uniq_id")] = serial + "_" + buttonId; root[F("uniq_id")] = serial + "_" + buttonId;
if (strcmp(icon, "")) { if (strcmp(icon, "")) {
root[F("ic")] = icon; root[F("ic")] = icon;
@ -226,7 +226,7 @@ void MqttHassPublishingClass::publishInverterBinarySensor(std::shared_ptr<Invert
String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic; String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
DynamicJsonDocument root(1024); DynamicJsonDocument root(1024);
root[F("name")] = caption; root[F("name")] = String(inv->name()) + " " + caption;
root[F("uniq_id")] = serial + "_" + sensorId; root[F("uniq_id")] = serial + "_" + sensorId;
root[F("stat_t")] = statTopic; root[F("stat_t")] = statTopic;
root[F("pl_on")] = payload_on; root[F("pl_on")] = payload_on;

View File

@ -5,6 +5,7 @@
#include "WebApi.h" #include "WebApi.h"
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h"
#include "defaults.h" #include "defaults.h"
WebApiClass::WebApiClass() WebApiClass::WebApiClass()
@ -28,6 +29,7 @@ void WebApiClass::init()
_webApiNetwork.init(&_server); _webApiNetwork.init(&_server);
_webApiNtp.init(&_server); _webApiNtp.init(&_server);
_webApiPower.init(&_server); _webApiPower.init(&_server);
_webApiPrometheus.init(&_server);
_webApiSecurity.init(&_server); _webApiSecurity.init(&_server);
_webApiSysstatus.init(&_server); _webApiSysstatus.init(&_server);
_webApiWebapp.init(&_server); _webApiWebapp.init(&_server);
@ -59,4 +61,22 @@ void WebApiClass::loop()
_webApiVedirect.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; WebApiClass WebApi;

View File

@ -6,6 +6,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "WebApi.h"
#include <LittleFS.h> #include <LittleFS.h>
void WebApiConfigClass::init(AsyncWebServer* server) void WebApiConfigClass::init(AsyncWebServer* server)
@ -32,11 +33,19 @@ void WebApiConfigClass::loop()
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true); request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true);
} }
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");
@ -93,6 +102,10 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
// the request handler is triggered after the upload has finished... // the request handler is triggered after the upload has finished...
// create the response, add header, and send response // 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) 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) { if (!index) {
// open the file on first call and store the file handle in the request object // open the file on first call and store the file handle in the request object
request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w"); request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w");

View File

@ -7,6 +7,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "Hoymiles.h" #include "Hoymiles.h"
#include "WebApi.h"
void WebApiDtuClass::init(AsyncWebServer* server) void WebApiDtuClass::init(AsyncWebServer* server)
{ {
@ -24,6 +25,10 @@ void WebApiDtuClass::loop()
void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -43,6 +48,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -7,6 +7,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "Update.h" #include "Update.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiFirmwareClass::init(AsyncWebServer* server) void WebApiFirmwareClass::init(AsyncWebServer* server)
@ -31,6 +32,10 @@ void WebApiFirmwareClass::loop()
void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
// the request handler is triggered after the upload has finished... // the request handler is triggered after the upload has finished...
// create the response, add header, and send response // 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) 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 // Upload handler chunks in data
if (!index) { if (!index) {
if (!request->hasParam("MD5", true)) { if (!request->hasParam("MD5", true)) {

View File

@ -8,6 +8,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "Hoymiles.h" #include "Hoymiles.h"
#include "MqttHassPublishing.h" #include "MqttHassPublishing.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiInverterClass::init(AsyncWebServer* server) void WebApiInverterClass::init(AsyncWebServer* server)
@ -28,6 +29,10 @@ void WebApiInverterClass::loop()
void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U); AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
JsonArray data = root.createNestedArray(F("inverter")); JsonArray data = root.createNestedArray(F("inverter"));
@ -48,13 +53,16 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
obj[F("serial")] = buffer; obj[F("serial")] = buffer;
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
uint8_t max_channels;
if (inv == nullptr) { if (inv == nullptr) {
obj[F("type")] = F("Unknown"); obj[F("type")] = F("Unknown");
max_channels = INV_MAX_CHAN_COUNT;
} else { } else {
obj[F("type")] = inv->typeName(); 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]; obj[F("max_power")][c] = config.Inverter[i].MaxChannelPower[c];
} }
} }
@ -66,6 +74,10 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");
@ -151,6 +163,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");
@ -210,7 +226,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
} }
JsonArray maxPowerArray = root[F("max_power")].as<JsonArray>(); JsonArray maxPowerArray = root[F("max_power")].as<JsonArray>();
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!"); retMsg[F("message")] = F("Invalid amount of max channel setting given!");
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -265,6 +281,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -6,6 +6,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Hoymiles.h" #include "Hoymiles.h"
#include "WebApi.h"
void WebApiLimitClass::init(AsyncWebServer* server) void WebApiLimitClass::init(AsyncWebServer* server)
{ {
@ -54,6 +55,10 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -8,6 +8,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "MqttHassPublishing.h" #include "MqttHassPublishing.h"
#include "MqttSettings.h" #include "MqttSettings.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiMqttClass::init(AsyncWebServer* server) void WebApiMqttClass::init(AsyncWebServer* server)
@ -54,6 +55,10 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -83,6 +88,10 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -7,6 +7,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiNetworkClass::init(AsyncWebServer* server) void WebApiNetworkClass::init(AsyncWebServer* server)
@ -52,6 +53,10 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -72,6 +77,10 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -7,6 +7,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "NtpSettings.h" #include "NtpSettings.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiNtpClass::init(AsyncWebServer* server) void WebApiNtpClass::init(AsyncWebServer* server)
@ -37,7 +38,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr;
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
root[F("ntp_status")] = false; root[F("ntp_status")] = false;
} else { } else {
root[F("ntp_status")] = true; root[F("ntp_status")] = true;
@ -52,6 +53,10 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -66,6 +71,10 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");
@ -142,11 +151,15 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
struct tm timeinfo; struct tm timeinfo;
if (!getLocalTime(&timeinfo, 0)) { if (!getLocalTime(&timeinfo, 5)) {
root[F("ntp_status")] = false; root[F("ntp_status")] = false;
} else { } else {
root[F("ntp_status")] = true; root[F("ntp_status")] = true;
@ -165,6 +178,10 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

View File

@ -6,6 +6,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Hoymiles.h" #include "Hoymiles.h"
#include "WebApi.h"
void WebApiPowerClass::init(AsyncWebServer* server) void WebApiPowerClass::init(AsyncWebServer* server)
{ {
@ -47,6 +48,10 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");

97
src/WebApi_prometheus.cpp Normal file
View File

@ -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<InverterAbstract> 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));
}
}

View File

@ -6,6 +6,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Configuration.h" #include "Configuration.h"
#include "WebApi.h"
#include "helper.h" #include "helper.h"
void WebApiSecurityClass::init(AsyncWebServer* server) 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_GET, std::bind(&WebApiSecurityClass::onPasswordGet, this, _1));
_server->on("/api/security/password", HTTP_POST, std::bind(&WebApiSecurityClass::onPasswordPost, 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() void WebApiSecurityClass::loop()
@ -24,6 +26,10 @@ void WebApiSecurityClass::loop()
void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request) void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
@ -36,6 +42,10 @@ void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request)
void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request) void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot(); JsonObject retMsg = response->getRoot();
retMsg[F("type")] = F("warning"); retMsg[F("type")] = F("warning");
@ -90,3 +100,18 @@ void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request)
response->setLength(); response->setLength();
request->send(response); 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);
}

View File

@ -14,23 +14,25 @@
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.2", "bootstrap": "^5.2.2",
"bootstrap-icons-vue": "^1.8.1", "bootstrap-icons-vue": "^1.8.1",
"mitt": "^3.0.0",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.2.41", "vue": "^3.2.45",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.2.0", "@rushstack/eslint-patch": "^1.2.0",
"@types/bootstrap": "^5.2.5", "@types/bootstrap": "^5.2.6",
"@types/node": "^18.11.8", "@types/node": "^18.11.9",
"@types/spark-md5": "^3.0.2", "@types/spark-md5": "^3.0.2",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.2",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"eslint": "^8.26.0", "eslint": "^8.27.0",
"eslint-plugin-vue": "^9.7.0", "eslint-plugin-vue": "^9.7.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.56.1",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"vite": "^3.2.2", "vite": "^3.2.3",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^2.1.1", "vite-plugin-css-injected-by-js": "^2.1.1",
"vue-tsc": "^1.0.9" "vue-tsc": "^1.0.9"

View File

@ -14,7 +14,7 @@ import { defineComponent } from 'vue';
import { formatNumber } from '@/utils'; import { formatNumber } from '@/utils';
declare interface LimitData { declare interface LimitData {
limit: number, limit: number;
} }
export default defineComponent({ export default defineComponent({

View File

@ -7,7 +7,7 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" ref="navbarCollapse" id="navbarNavAltMarkup"> <div class="collapse navbar-collapse" ref="navbarCollapse" id="navbarNavAltMarkup">
<ul class="navbar-nav"> <ul class="navbar-nav me-auto">
<li class="nav-item"> <li class="nav-item">
<router-link @click="onClick" class="nav-link" to="/">Live Data</router-link> <router-link @click="onClick" class="nav-link" to="/">Live Data</router-link>
</li> </li>
@ -78,6 +78,10 @@
<router-link @click="onClick" class="nav-link" to="/about">About</router-link> <router-link @click="onClick" class="nav-link" to="/about">About</router-link>
</li> </li>
</ul> </ul>
<form class="d-flex" role="search">
<button v-if="isLogged" class="btn btn-outline-danger" @click="signout">Logout</button>
<button v-if="!isLogged" class="btn btn-outline-success" @click="signin">Login</button>
</form>
</div> </div>
</div> </div>
</nav> </nav>
@ -85,13 +89,39 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { logout, isLoggedIn } from '@/utils/authentication';
import { BIconSun } from 'bootstrap-icons-vue'; import { BIconSun } from 'bootstrap-icons-vue';
export default defineComponent({ export default defineComponent({
components: { components: {
BIconSun, BIconSun,
}, },
data() {
return {
isLogged: this.isLoggedIn(),
}
},
created() {
this.$emitter.on("logged-in", () => {
this.isLogged = this.isLoggedIn();
});
this.$emitter.on("logged-out", () => {
this.isLogged = this.isLoggedIn();
});
},
methods: { methods: {
isLoggedIn,
logout,
signin(e: Event) {
e.preventDefault();
this.$router.push('/login');
},
signout(e: Event) {
e.preventDefault();
this.logout();
this.$emitter.emit("logged-out");
this.$router.push('/');
},
onClick() { onClick() {
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show"); this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");
} }

9
webapp/src/emitter.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import mitt from 'mitt';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$emitter: Emitter;
}
}
export { } // Important! See note.

View File

@ -1,12 +1,16 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import mitt from 'mitt';
import "bootstrap/dist/css/bootstrap.min.css" import './scss/styles.scss'
import "bootstrap" import "bootstrap"
const app = createApp(App) const app = createApp(App)
const emitter = mitt();
app.config.globalProperties.$emitter = emitter;
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -15,6 +15,7 @@ import ConfigAdminView from '@/views/ConfigAdminView.vue'
import VedirectAdminView from '@/views/VedirectAdminView.vue' import VedirectAdminView from '@/views/VedirectAdminView.vue'
import VedirectInfoView from '@/views/VedirectInfoView.vue' import VedirectInfoView from '@/views/VedirectInfoView.vue'
import SecurityAdminView from '@/views/SecurityAdminView.vue' import SecurityAdminView from '@/views/SecurityAdminView.vue'
import LoginView from '@/views/LoginView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -25,6 +26,11 @@ const router = createRouter({
name: 'Home', name: 'Home',
component: HomeView component: HomeView
}, },
{
path: '/login',
name: 'Login',
component: LoginView
},
{ {
path: '/about', path: '/about',
name: 'About', name: 'About',
@ -101,6 +107,22 @@ const router = createRouter({
component: SecurityAdminView component: SecurityAdminView
} }
] ]
}) });
router.beforeEach((to, from, next) => {
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/', '/login', '/about', '/info/network', '/info/system', '/info/ntp', '/info/mqtt', ];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
if (authRequired && !loggedIn) {
return next({
path: '/login',
query: { returnUrl: to.path }
});
}
next();
});
export default router; export default router;

View File

@ -0,0 +1,2 @@
// Import all of Bootstrap's CSS
@import "~bootstrap/scss/bootstrap";

View File

@ -1,10 +1,10 @@
export interface DevInfoStatus { export interface DevInfoStatus {
valid_data: boolean, valid_data: boolean;
fw_bootloader_version: number, fw_bootloader_version: number;
fw_build_version: number, fw_build_version: number;
fw_build_datetime: Date, fw_build_datetime: Date;
hw_part_number: number, hw_part_number: number;
hw_version: number, hw_version: number;
hw_model_name: string, hw_model_name: string;
max_power: number, max_power: number;
} }

View File

@ -1,5 +1,5 @@
export interface DtuConfig { export interface DtuConfig {
dtu_serial: number, dtu_serial: number;
dtu_pollinterval: number, dtu_pollinterval: number;
dtu_palevel: number dtu_palevel: number;
} }

View File

@ -1,11 +1,11 @@
export interface EventlogItem { export interface EventlogItem {
message_id: number, message_id: number;
message: string, message: string;
start_time: number, start_time: number;
end_time: number end_time: number;
} }
export interface EventlogItems { export interface EventlogItems {
count: number, count: number;
events: Array<EventlogItem>, events: Array<EventlogItem>;
} }

View File

@ -1,5 +1,5 @@
export interface LimitConfig { export interface LimitConfig {
serial: number, serial: number;
limit_value: number, limit_value: number;
limit_type: number limit_type: number;
} }

View File

@ -1,5 +1,5 @@
export interface LimitStatus { export interface LimitStatus {
limit_relative: number, limit_relative: number;
max_power: number, max_power: number;
limit_set_status: string, limit_set_status: string;
} }

View File

@ -1,43 +1,43 @@
export interface ValueObject { export interface ValueObject {
v: number, // value v: number; // value
u: string, // unit u: string; // unit
d: number, // digits d: number; // digits
}; };
export interface InverterStatistics { export interface InverterStatistics {
Power?: ValueObject, Power?: ValueObject;
Voltage?: ValueObject, Voltage?: ValueObject;
Current?: ValueObject, Current?: ValueObject;
"Power DC"?: ValueObject, "Power DC"?: ValueObject;
YieldDay?: ValueObject, YieldDay?: ValueObject;
YieldTotal?: ValueObject, YieldTotal?: ValueObject;
Frequency?: ValueObject, Frequency?: ValueObject;
Temperature?: ValueObject, Temperature?: ValueObject;
PowerFactor?: ValueObject, PowerFactor?: ValueObject;
ReactivePower?: ValueObject, ReactivePower?: ValueObject;
Efficiency?: ValueObject, Efficiency?: ValueObject;
Irradiation?: ValueObject, Irradiation?: ValueObject;
} }
export interface Inverter { export interface Inverter {
serial: number, serial: number;
name: string, name: string;
data_age: number, data_age: number;
reachable: boolean, reachable: boolean;
producing: boolean, producing: boolean;
limit_relative: number, limit_relative: number;
limit_absolute: number, limit_absolute: number;
events: number, events: number;
[key: number]: InverterStatistics, [key: number]: InverterStatistics;
}; };
export interface Total { export interface Total {
Power: ValueObject, Power: ValueObject;
YieldDay: ValueObject, YieldDay: ValueObject;
YieldTotal: ValueObject, YieldTotal: ValueObject;
}; };
export interface LiveData { export interface LiveData {
inverters: Inverter[], inverters: Inverter[];
total: Total, total: Total;
} }

View File

@ -1,20 +1,20 @@
export interface MqttConfig { export interface MqttConfig {
mqtt_enabled: boolean, mqtt_enabled: boolean;
mqtt_hostname: string, mqtt_hostname: string;
mqtt_port: number, mqtt_port: number;
mqtt_username: string, mqtt_username: string;
mqtt_password: string, mqtt_password: string;
mqtt_topic: string, mqtt_topic: string;
mqtt_publish_interval: number, mqtt_publish_interval: number;
mqtt_retain: boolean, mqtt_retain: boolean;
mqtt_tls: boolean, mqtt_tls: boolean;
mqtt_root_ca_cert: string, mqtt_root_ca_cert: string;
mqtt_lwt_topic: string, mqtt_lwt_topic: string;
mqtt_lwt_online: string, mqtt_lwt_online: string;
mqtt_lwt_offline: string, mqtt_lwt_offline: string;
mqtt_hass_enabled: boolean, mqtt_hass_enabled: boolean;
mqtt_hass_expire: boolean, mqtt_hass_expire: boolean;
mqtt_hass_retain: boolean, mqtt_hass_retain: boolean;
mqtt_hass_topic: string, mqtt_hass_topic: string;
mqtt_hass_individualpanels: boolean mqtt_hass_individualpanels: boolean;
} }

View File

@ -1,17 +1,17 @@
export interface MqttStatus { export interface MqttStatus {
mqtt_enabled: boolean, mqtt_enabled: boolean;
mqtt_hostname: string, mqtt_hostname: string;
mqtt_port: number, mqtt_port: number;
mqtt_username: string, mqtt_username: string;
mqtt_topic: string, mqtt_topic: string;
mqtt_publish_interval: number, mqtt_publish_interval: number;
mqtt_retain: boolean, mqtt_retain: boolean;
mqtt_tls: boolean, mqtt_tls: boolean;
mqtt_root_ca_cert_info: string, mqtt_root_ca_cert_info: string;
mqtt_connected: boolean, mqtt_connected: boolean;
mqtt_hass_enabled: boolean, mqtt_hass_enabled: boolean;
mqtt_hass_expire: boolean, mqtt_hass_expire: boolean;
mqtt_hass_retain: boolean, mqtt_hass_retain: boolean;
mqtt_hass_topic: string, mqtt_hass_topic: string;
mqtt_hass_individualpanels: boolean mqtt_hass_individualpanels: boolean;
} }

View File

@ -1,22 +1,22 @@
export interface NetworkStatus { export interface NetworkStatus {
// WifiStationInfo // WifiStationInfo
sta_status: boolean, sta_status: boolean;
sta_ssid: string, sta_ssid: string;
sta_rssi: number, sta_rssi: number;
// WifiApInfo // WifiApInfo
ap_status: boolean, ap_status: boolean;
ap_ssid: string, ap_ssid: string;
ap_stationnum: number, ap_stationnum: number;
// InterfaceNetworkInfo // InterfaceNetworkInfo
network_hostname: string, network_hostname: string;
network_ip: string, network_ip: string;
network_netmask: string, network_netmask: string;
network_gateway: string, network_gateway: string;
network_dns1: string, network_dns1: string;
network_dns2: string, network_dns2: string;
network_mac: string, network_mac: string;
network_mode: string, network_mode: string;
// InterfaceApInfo // InterfaceApInfo
ap_ip: string, ap_ip: string;
ap_mac: string, ap_mac: string;
} }

View File

@ -1,11 +1,11 @@
export interface NetworkConfig { export interface NetworkConfig {
ssid: string, ssid: string;
password: string, password: string;
hostname: string, hostname: string;
dhcp: boolean, dhcp: boolean;
ipaddress: string, ipaddress: string;
netmask: string, netmask: string;
gateway: string, gateway: string;
dns1: string, dns1: string;
dns2: string dns2: string;
} }

View File

@ -1,5 +1,5 @@
export interface NtpConfig { export interface NtpConfig {
ntp_server: string, ntp_server: string;
ntp_timezone: string, ntp_timezone: string;
ntp_timezone_descr: string ntp_timezone_descr: string;
} }

View File

@ -1,7 +1,7 @@
export interface NtpStatus { export interface NtpStatus {
ntp_server: string, ntp_server: string;
ntp_timezone: string, ntp_timezone: string;
ntp_timezone_descr: string ntp_timezone_descr: string
ntp_status: boolean, ntp_status: boolean;
ntp_localtime: string ntp_localtime: string;
} }

View File

@ -1,3 +1,3 @@
export interface SecurityConfig { export interface SecurityConfig {
password: string password: string;
} }

View File

@ -1,29 +1,29 @@
export interface SystemStatus { export interface SystemStatus {
// HardwareInfo // HardwareInfo
chipmodel: string, chipmodel: string;
chiprevision: number, chiprevision: number;
chipcores: number, chipcores: number;
cpufreq: number, cpufreq: number;
// FirmwareInfo // FirmwareInfo
hostname: string, hostname: string;
sdkversion: string, sdkversion: string;
config_version: string, config_version: string;
git_hash: string, git_hash: string;
resetreason_0: string, resetreason_0: string;
resetreason_1: string, resetreason_1: string;
cfgsavecount: number, cfgsavecount: number;
uptime: number, uptime: number;
update_text: string, update_text: string;
update_url: string, update_url: string;
update_status: string, update_status: string;
// MemoryInfo // MemoryInfo
heap_total: number, heap_total: number;
heap_used: number, heap_used: number;
littlefs_total: number, littlefs_total: number;
littlefs_used: number, littlefs_used: number;
sketch_total: number, sketch_total: number;
sketch_used: number, sketch_used: number;
// RadioInfo // RadioInfo
radio_connected: boolean, radio_connected: boolean;
radio_pvariant: boolean, radio_pvariant: boolean;
} }

View File

@ -1,5 +1,5 @@
export interface VedirectConfig { export interface VedirectConfig {
vedirect_enabled: boolean, vedirect_enabled: boolean;
vedirect_pollinterval: number, vedirect_pollinterval: number;
vedirect_updatesonly: boolean vedirect_updatesonly: boolean;
} }

View File

@ -2,24 +2,24 @@ import type { ValueObject } from '@/types/LiveDataStatus';
// Ve.Direct // Ve.Direct
export interface Vedirect { export interface Vedirect {
SER: string, SER: string;
PID: string, PID: string;
FW: string, FW: string;
age_critical: boolean, age_critical: boolean;
data_age: 0, data_age: 0;
LOAD: ValueObject, LOAD: ValueObject;
CS: ValueObject, CS: ValueObject;
MPPT: ValueObject, MPPT: ValueObject;
OR: ValueObject, OR: ValueObject;
ERR: ValueObject, ERR: ValueObject;
HSDS: ValueObject, HSDS: ValueObject;
V: ValueObject, V: ValueObject;
I: ValueObject, I: ValueObject;
VPV: ValueObject, VPV: ValueObject;
PPV: ValueObject, PPV: ValueObject;
H19: ValueObject, H19: ValueObject;
H20: ValueObject, H20: ValueObject;
H21: ValueObject, H21: ValueObject;
H22: ValueObject, H22: ValueObject;
H23: ValueObject, H23: ValueObject;
} }

View File

@ -1,4 +1,4 @@
export interface VedirectStatus { export interface VedirectStatus {
vedirect_enabled: boolean, vedirect_enabled: boolean;
vedirect_updatesonly: boolean vedirect_updatesonly: boolean;
} }

View File

@ -0,0 +1,84 @@
import type { Emitter, EventType } from "mitt";
export function authHeader(): Headers {
// return authorization header with basic auth credentials
let user = JSON.parse(localStorage.getItem('user') || "");
if (user && user.authdata) {
const headers = new Headers();
headers.append('Authorization', 'Basic ' + user.authdata);
headers.append('X-Requested-With', 'XMLHttpRequest')
return new Headers(headers);
} else {
return new Headers();
}
}
export function logout() {
// remove user from local storage to log user out
localStorage.removeItem('user');
}
export function isLoggedIn(): boolean {
return (localStorage.getItem('user') != null);
}
export function login(username: String, password: String) {
const requestOptions = {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Authorization': 'Basic ' + btoa(unescape(encodeURIComponent(username + ':' + password))),
},
};
return fetch('/api/security/authenticate', requestOptions)
.then(handleAuthResponse)
.then(retVal => {
// login successful if there's a user in the response
if (retVal) {
// store user details and basic auth credentials in local storage
// to keep user logged in between page refreshes
retVal.authdata = btoa(unescape(encodeURIComponent(username + ':' + password)));
localStorage.setItem('user', JSON.stringify(retVal));
}
return retVal;
});
}
export function handleResponse(response: Response, emitter: Emitter<Record<EventType, unknown>>) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if (response.status === 401) {
// auto logout if 401 response returned from api
logout();
emitter.emit("logged-out");
location.reload();
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
function handleAuthResponse(response: Response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if (response.status === 401) {
// auto logout if 401 response returned from api
logout();
}
const error = "Invalid credentials";
return Promise.reject(error);
}
return data;
});
}

View File

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

View File

@ -112,6 +112,7 @@ import {
} from 'bootstrap-icons-vue'; } from 'bootstrap-icons-vue';
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -152,15 +153,10 @@ export default defineComponent({
fetch("/api/config/delete", { fetch("/api/config/delete", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;
@ -171,10 +167,17 @@ export default defineComponent({
this.modalFactoryReset.hide(); this.modalFactoryReset.hide();
}, },
downloadConfig() { downloadConfig() {
const link = document.createElement('a') fetch("/api/config/get", { headers: authHeader() })
link.href = "/api/config/get" .then(res => res.blob())
link.download = 'config.json' .then(blob => {
link.click() var file = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = file;
a.download = "config.json";
document.body.appendChild(a);
a.click();
a.remove();
});
}, },
uploadConfig(event: Event | null) { uploadConfig(event: Event | null) {
this.uploading = true; this.uploading = true;
@ -206,6 +209,9 @@ export default defineComponent({
formData.append("config", this.file, "config"); formData.append("config", this.file, "config");
request.open("post", "/api/config/upload"); request.open("post", "/api/config/upload");
authHeader().forEach((value, key) => {
request.setRequestHeader(key, value);
});
request.send(formData); request.send(formData);
}, },
clear() { clear() {

View File

@ -49,6 +49,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { DtuConfig } from "@/types/DtuConfig"; import type { DtuConfig } from "@/types/DtuConfig";
export default defineComponent({ export default defineComponent({
@ -77,8 +78,8 @@ export default defineComponent({
methods: { methods: {
getDtuConfig() { getDtuConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/dtu/config") fetch("/api/dtu/config", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then( .then(
(data) => { (data) => {
this.dtuConfigList = data; this.dtuConfigList = data;
@ -94,15 +95,10 @@ export default defineComponent({
fetch("/api/dtu/config", { fetch("/api/dtu/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -77,6 +77,7 @@ import {
BIconArrowRepeat, BIconArrowRepeat,
BIconCheckCircle BIconCheckCircle
} from 'bootstrap-icons-vue'; } from 'bootstrap-icons-vue';
import { authHeader } from '@/utils/authentication';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -160,6 +161,9 @@ export default defineComponent({
formData.append("MD5", (md5 as string)); formData.append("MD5", (md5 as string));
formData.append("firmware", this.file, "firmware"); formData.append("firmware", this.file, "firmware");
request.open("post", "/api/firmware/update"); request.open("post", "/api/firmware/update");
authHeader().forEach((value, key) => {
request.setRequestHeader(key, value);
});
request.send(formData); request.send(formData);
}) })
.catch(() => { .catch(() => {

View File

@ -50,7 +50,7 @@
</div> </div>
<div class="btn-toolbar p-2" role="toolbar"> <div class="btn-toolbar p-2" role="toolbar">
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-danger" <button :disabled="!isLogged" type="button" class="btn btn-sm btn-danger"
@click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit"> @click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit">
<BIconSpeedometer style="font-size:24px;" /> <BIconSpeedometer style="font-size:24px;" />
@ -58,7 +58,7 @@
</div> </div>
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-danger" <button :disabled="!isLogged" type="button" class="btn btn-sm btn-danger"
@click="onShowPowerSettings(inverter.serial)" title="Turn Inverter on/off"> @click="onShowPowerSettings(inverter.serial)" title="Turn Inverter on/off">
<BIconPower style="font-size:24px;" /> <BIconPower style="font-size:24px;" />
@ -340,6 +340,7 @@ import type { EventlogItems } from '@/types/EventlogStatus';
import type { LiveData, Inverter } from '@/types/LiveDataStatus'; import type { LiveData, Inverter } from '@/types/LiveDataStatus';
import type { LimitStatus } from '@/types/LimitStatus'; import type { LimitStatus } from '@/types/LimitStatus';
import type { LimitConfig } from '@/types/LimitConfig'; import type { LimitConfig } from '@/types/LimitConfig';
import { isLoggedIn, handleResponse, authHeader } from '@/utils/authentication';
import { formatNumber } from '@/utils'; import { formatNumber } from '@/utils';
export default defineComponent({ export default defineComponent({
@ -364,6 +365,8 @@ export default defineComponent({
}, },
data() { data() {
return { return {
isLogged: this.isLoggedIn(),
socket: {} as WebSocket, socket: {} as WebSocket,
heartInterval: 0, heartInterval: 0,
dataAgeInterval: 0, dataAgeInterval: 0,
@ -406,6 +409,12 @@ export default defineComponent({
this.getInitialData(); this.getInitialData();
this.initSocket(); this.initSocket();
this.initDataAgeing(); this.initDataAgeing();
this.$emitter.on("logged-in", () => {
this.isLogged = this.isLoggedIn();
});
this.$emitter.on("logged-out", () => {
this.isLogged = this.isLoggedIn();
});
}, },
mounted() { mounted() {
this.eventLogView = new bootstrap.Modal('#eventView'); this.eventLogView = new bootstrap.Modal('#eventView');
@ -449,6 +458,7 @@ export default defineComponent({
}, },
methods: { methods: {
formatNumber, formatNumber,
isLoggedIn,
getInitialData() { getInitialData() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/livedata/status") fetch("/api/livedata/status")
@ -568,15 +578,10 @@ export default defineComponent({
fetch("/api/limit/config", { fetch("/api/limit/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
if (response.type == "success") { if (response.type == "success") {
@ -643,15 +648,10 @@ export default defineComponent({
fetch("/api/power/config", { fetch("/api/power/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
if (response.type == "success") { if (response.type == "success") {

View File

@ -1,7 +1,7 @@
<template> <template>
<BasePage :title="'Inverter Settings'" :isLoading="dataLoading"> <BasePage :title="'Inverter Settings'" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType"> <BootstrapAlert v-model="alert.show" dismissible :variant="alert.type">
{{ alertMessage }} {{ alert.message }}
</BootstrapAlert> </BootstrapAlert>
<div class="card"> <div class="card">
@ -10,12 +10,12 @@
<form class="form-inline" v-on:submit.prevent="onSubmit"> <form class="form-inline" v-on:submit.prevent="onSubmit">
<div class="form-group"> <div class="form-group">
<label>Serial</label> <label>Serial</label>
<input v-model="inverterData.serial" type="number" class="form-control ml-sm-2 mr-sm-4 my-2" <input v-model="newInverterData.serial" type="number" class="form-control ml-sm-2 mr-sm-4 my-2"
required /> required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Name</label> <label>Name</label>
<input v-model="inverterData.name" type="text" class="form-control ml-sm-2 mr-sm-4 my-2" <input v-model="newInverterData.name" type="text" class="form-control ml-sm-2 mr-sm-4 my-2"
maxlength="31" required /> maxlength="31" required />
</div> </div>
<div class="ml-auto text-right"> <div class="ml-auto text-right">
@ -44,22 +44,15 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="inverter in sortedInverters" v-bind:key="inverter.id"> <tr v-for="inverter in sortedInverters" v-bind:key="inverter.id">
<td>{{ inverter.serial }}</td>
<td> <td>{{ inverter.name }}</td>
{{ inverter.serial }} <td>{{ inverter.type }}</td>
</td>
<td>
{{ inverter.name }}
</td>
<td>
{{ inverter.type }}
</td>
<td> <td>
<a href="#" class="icon text-danger" title="Delete inverter"> <a href="#" class="icon text-danger" title="Delete inverter">
<BIconTrash v-on:click="onDeleteModal(inverter)" /> <BIconTrash v-on:click="onOpenModal(modalDelete, inverter)" />
</a>&nbsp; </a>&nbsp;
<a href="#" class="icon" title="Edit inverter"> <a href="#" class="icon" title="Edit inverter">
<BIconPencil v-on:click="onEdit(inverter)" /> <BIconPencil v-on:click="onOpenModal(modal, inverter)" />
</a> </a>
</td> </td>
</tr> </tr>
@ -78,39 +71,36 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<div class="mb-3"> <div class="mb-3">
<label for="inverter-serial" class="col-form-label">Serial:</label> <label for="inverter-serial" class="col-form-label">Serial:</label>
<input v-model="editInverterData.serial" type="number" id="inverter-serial" <input v-model="selectedInverterData.serial" type="number" id="inverter-serial"
class="form-control" /> class="form-control" />
</div>
<div class="mb-3">
<label for="inverter-name" class="col-form-label">Name:</label> <label for="inverter-name" class="col-form-label">Name:</label>
<input v-model="editInverterData.name" type="text" id="inverter-name" class="form-control" <input v-model="selectedInverterData.name" type="text" id="inverter-name"
maxlength="31" /> class="form-control" maxlength="31" />
</div> </div>
<div class="mb-3" v-for="(max, index) in editInverterData.max_power" :key="`${index}`"> <div v-for="(max, index) in selectedInverterData.max_power" :key="`${index}`">
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index + <label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +1 }}:</label>
1 <div class="d-flex mb-2">
}}:</label>
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0" <input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
v-model="editInverterData.max_power[index]" v-model="selectedInverterData.max_power[index]"
:aria-describedby="`inverter-maxDescription_${index} inverter-maxHelpText_${index}`" /> :aria-describedby="`inverter-maxDescription_${index} inverter-customizer`" />
<span class="input-group-text" :id="`inverter-maxDescription_${index}`">W</span> <span class="input-group-text" :id="`inverter-maxDescription_${index}`">W<sup>*</sup></span>
</div> </div>
<div :id="`inverter-maxHelpText_${index}`" class="form-text">This value is used to
calculate the Irradiation.</div>
</div> </div>
</div>
<div :id="`inverter-customizer`" class="form-text">*) Input the kWp of the channel to
calculate irradiation.</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="onCancel" <button type="button" class="btn btn-secondary" @click="onCloseModal(modal)"
data-bs-dismiss="modal">Cancel</button> data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" @click="onEditSubmit(editId)">Save <button type="button" class="btn btn-primary" @click="onEditSubmit">Save
changes</button> changes</button>
</div> </div>
</div> </div>
@ -125,13 +115,13 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Are you sure you want to delete the inverter "{{ deleteInverterData.name }}" with serial number Are you sure you want to delete the inverter "{{ selectedInverterData.name }}" with serial number
{{ deleteInverterData.serial }}? {{ selectedInverterData.serial }}?
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="onDeleteCancel" <button type="button" class="btn btn-secondary" @click="onCloseModal(modalDelete)"
data-bs-dismiss="modal">Cancel</button> data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" @click="onDelete(deleteId.toString())">Delete</button> <button type="button" class="btn btn-danger" @click="onDelete">Delete</button>
</div> </div>
</div> </div>
</div> </div>
@ -147,13 +137,20 @@ import {
} from 'bootstrap-icons-vue'; } from 'bootstrap-icons-vue';
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
declare interface Inverter { declare interface Inverter {
id: string, id: string;
serial: number, serial: number;
name: string, name: string;
type: string type: string;
max_power: number[] max_power: number[];
}
declare interface AlertResponse {
message: string;
type: string;
show: boolean;
} }
export default defineComponent({ export default defineComponent({
@ -167,16 +164,11 @@ export default defineComponent({
return { return {
modal: {} as bootstrap.Modal, modal: {} as bootstrap.Modal,
modalDelete: {} as bootstrap.Modal, modalDelete: {} as bootstrap.Modal,
deleteId: -1, newInverterData: {} as Inverter,
editId: "-1", selectedInverterData: {} as Inverter,
inverterData: {} as Inverter,
editInverterData: {} as Inverter,
deleteInverterData: {} as Inverter,
inverters: [] as Inverter[], inverters: [] as Inverter[],
dataLoading: true, dataLoading: true,
alertMessage: "", alert: {} as AlertResponse
alertType: "info",
showAlert: false,
}; };
}, },
mounted() { mounted() {
@ -196,130 +188,49 @@ export default defineComponent({
methods: { methods: {
getInverters() { getInverters() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/inverter/list") fetch("/api/inverter/list", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then((data) => { .then((data) => {
this.inverters = data.inverter; this.inverters = data.inverter;
this.dataLoading = false; this.dataLoading = false;
}); });
}, },
callInverterApiEndpoint(endpoint: string, jsonData: string) {
const formData = new FormData();
formData.append("data", jsonData);
fetch("/api/inverter/" + endpoint, {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter))
.then((data) => {
this.getInverters();
this.alert = data;
this.alert.show = true;
});
},
onSubmit() { onSubmit() {
const formData = new FormData(); this.callInverterApiEndpoint("add", JSON.stringify(this.newInverterData));
formData.append("data", JSON.stringify(this.inverterData)); this.newInverterData = {} as Inverter;
},
fetch("/api/inverter/add", { onDelete() {
method: "POST", this.callInverterApiEndpoint("del", JSON.stringify({ id: this.selectedInverterData.id }));
body: formData, this.onCloseModal(this.modalDelete);
}) },
.then(function (response) { onEditSubmit() {
if (response.status != 200) { this.callInverterApiEndpoint("edit", JSON.stringify(this.selectedInverterData));
throw response.status; this.onCloseModal(this.modal);
} else { },
return response.json(); onOpenModal(modal: bootstrap.Modal, inverter: Inverter) {
// deep copy inverter object for editing/deleting
this.selectedInverterData = JSON.parse(JSON.stringify(inverter)) as Inverter;
modal.show();
},
onCloseModal(modal: bootstrap.Modal) {
modal.hide();
} }
})
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
)
.then(() => { this.getInverters() });
this.inverterData.serial = 0;
this.inverterData.name = "";
},
onDeleteModal(inverter: Inverter) {
this.modalDelete.show();
this.deleteInverterData.serial = inverter.serial;
this.deleteInverterData.name = inverter.name;
this.deleteInverterData.type = inverter.type;
this.deleteId = +inverter.id;
},
onDeleteCancel() {
this.deleteId = -1;
this.deleteInverterData.serial = 0;
this.deleteInverterData.name = "";
this.deleteInverterData.max_power = [];
this.modalDelete.hide();
},
onDelete(id: string) {
const formData = new FormData();
formData.append("data", JSON.stringify({ id: id }));
fetch("/api/inverter/del", {
method: "POST",
body: formData,
})
.then(function (response) {
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
)
.then(() => { this.getInverters() });
this.deleteId = -1;
this.deleteInverterData.serial = 0;
this.deleteInverterData.name = "";
this.deleteInverterData.max_power = [];
this.modalDelete.hide();
},
onEdit(inverter: Inverter) {
this.modal.show();
this.editId = inverter.id;
this.editInverterData.serial = inverter.serial;
this.editInverterData.name = inverter.name;
this.editInverterData.type = inverter.type;
this.editInverterData.max_power = inverter.max_power;
},
onCancel() {
this.editId = "-1";
this.editInverterData.serial = 0;
this.editInverterData.name = "";
this.editInverterData.max_power = [];
this.modal.hide();
},
onEditSubmit(id: string) {
const formData = new FormData();
this.editInverterData.id = id;
formData.append("data", JSON.stringify(this.editInverterData));
fetch("/api/inverter/edit", {
method: "POST",
body: formData,
})
.then(function (response) {
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then(
(response) => {
this.alertMessage = response.message;
this.alertType = response.type;
this.showAlert = true;
}
)
.then(() => { this.getInverters() });
this.editId = "-1";
this.editInverterData.serial = 0;
this.editInverterData.name = "";
this.editInverterData.type = "";
this.editInverterData.max_power = [];
this.modal.hide();
},
}, },
}); });
</script> </script>

View File

@ -0,0 +1,89 @@
<template>
<BasePage :title="'Login'" :isLoading="dataLoading">
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
{{ alertMessage }}
</BootstrapAlert>
<div class="card">
<div class="card-header text-bg-danger">System Login</div>
<div class="card-body">
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="username">Username</label>
<input type="text" v-model="username" name="username" class="form-control"
:class="{ 'is-invalid': submitted && !username }" />
<div v-show="submitted && !username" class="invalid-feedback">Username is required</div>
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" v-model="password" name="password" class="form-control"
:class="{ 'is-invalid': submitted && !password }" />
<div v-show="submitted && !password" class="invalid-feedback">Password is required</div>
</div>
<div class="form-group">
<button class="btn btn-primary" :disabled="dataLoading">Login</button>
</div>
</form>
</div>
</div>
</BasePage>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import router from '@/router';
import { login } from '@/utils';
import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
export default defineComponent({
components: {
BasePage,
BootstrapAlert,
},
data() {
return {
dataLoading: false,
alertMessage: "",
alertType: "info",
showAlert: false,
returnUrl: '',
username: '',
password: '',
submitted: false,
};
},
created() {
// get return url from route parameters or default to '/'
this.returnUrl = this.$route.query.returnUrl?.toString() || '/';
},
methods: {
handleSubmit(e: Event) {
this.submitted = true;
const { username, password } = this;
// stop here if form is invalid
if (!(username && password)) {
return;
}
this.dataLoading = true;
login(username, password)
.then(
() => {
this.$emitter.emit("logged-in");
router.push(this.returnUrl);
},
error => {
this.$emitter.emit("logged-out");
this.alertMessage = error;
this.alertType = 'danger';
this.showAlert = true;
this.dataLoading = false;
}
)
}
}
});
</script>

View File

@ -218,6 +218,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { MqttConfig } from "@/types/MqttConfig"; import type { MqttConfig } from "@/types/MqttConfig";
export default defineComponent({ export default defineComponent({
@ -240,8 +241,8 @@ export default defineComponent({
methods: { methods: {
getMqttConfig() { getMqttConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/mqtt/config") fetch("/api/mqtt/config", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then((data) => { .then((data) => {
this.mqttConfigList = data; this.mqttConfigList = data;
this.dataLoading = false; this.dataLoading = false;
@ -255,15 +256,10 @@ export default defineComponent({
fetch("/api/mqtt/config", { fetch("/api/mqtt/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -104,6 +104,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { NetworkConfig } from "@/types/NetworkkConfig"; import type { NetworkConfig } from "@/types/NetworkkConfig";
export default defineComponent({ export default defineComponent({
@ -126,8 +127,8 @@ export default defineComponent({
methods: { methods: {
getNetworkConfig() { getNetworkConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/network/config") fetch("/api/network/config", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then((data) => { .then((data) => {
this.networkConfigList = data; this.networkConfigList = data;
this.dataLoading = false; this.dataLoading = false;
@ -141,15 +142,10 @@ export default defineComponent({
fetch("/api/network/config", { fetch("/api/network/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -75,6 +75,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { NtpConfig } from "@/types/NtpConfig"; import type { NtpConfig } from "@/types/NtpConfig";
export default defineComponent({ export default defineComponent({
@ -127,8 +128,8 @@ export default defineComponent({
}, },
getNtpConfig() { getNtpConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/ntp/config") fetch("/api/ntp/config", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then( .then(
(data) => { (data) => {
this.ntpConfigList = data; this.ntpConfigList = data;
@ -142,8 +143,8 @@ export default defineComponent({
}, },
getCurrentTime() { getCurrentTime() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/ntp/time") fetch("/api/ntp/time", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then( .then(
(data) => { (data) => {
this.mcuTime = new Date( this.mcuTime = new Date(
@ -168,15 +169,10 @@ export default defineComponent({
fetch("/api/ntp/time", { fetch("/api/ntp/time", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;
@ -196,15 +192,10 @@ export default defineComponent({
fetch("/api/ntp/config", { fetch("/api/ntp/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -26,8 +26,8 @@
<div class="alert alert-secondary" role="alert"> <div class="alert alert-secondary" role="alert">
<b>Hint:</b> <b>Hint:</b>
The administrator password is used to connect to the device when in AP mode. The administrator password is used to access this web interface (user 'admin'), but also to
It must be 8..64 characters. connect to the device when in AP mode. It must be 8..64 characters.
</div> </div>
</div> </div>
@ -41,6 +41,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { SecurityConfig } from '@/types/SecurityConfig'; import type { SecurityConfig } from '@/types/SecurityConfig';
export default defineComponent({ export default defineComponent({
@ -65,8 +66,8 @@ export default defineComponent({
methods: { methods: {
getPasswordConfig() { getPasswordConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/security/password") fetch("/api/security/password", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then( .then(
(data) => { (data) => {
this.securityConfigList = data; this.securityConfigList = data;
@ -90,15 +91,10 @@ export default defineComponent({
fetch("/api/security/password", { fetch("/api/security/password", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -51,6 +51,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import BasePage from '@/components/BasePage.vue'; import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import { handleResponse, authHeader } from '@/utils/authentication';
import type { VedirectConfig } from "@/types/VedirectConfig"; import type { VedirectConfig } from "@/types/VedirectConfig";
export default defineComponent({ export default defineComponent({
@ -73,8 +74,8 @@ export default defineComponent({
methods: { methods: {
getVedirectConfig() { getVedirectConfig() {
this.dataLoading = true; this.dataLoading = true;
fetch("/api/vedirect/config") fetch("api/vedirect/config", { headers: authHeader() })
.then((response) => response.json()) .then((response) => handleResponse(response, this.$emitter))
.then((data) => { .then((data) => {
this.vedirectConfigList = data; this.vedirectConfigList = data;
this.dataLoading = false; this.dataLoading = false;
@ -88,15 +89,10 @@ export default defineComponent({
fetch("/api/vedirect/config", { fetch("/api/vedirect/config", {
method: "POST", method: "POST",
headers: authHeader(),
body: formData, body: formData,
}) })
.then(function (response) { .then((response) => handleResponse(response, this.$emitter))
if (response.status != 200) {
throw response.status;
} else {
return response.json();
}
})
.then( .then(
(response) => { (response) => {
this.alertMessage = response.message; this.alertMessage = response.message;

View File

@ -6,6 +6,8 @@ import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'; import viteCompression from 'vite-plugin-compression';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
const path = require('path')
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -14,7 +16,8 @@ export default defineConfig({
cssInjectedByJsPlugin()], cssInjectedByJsPlugin()],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url)),
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
} }
}, },
build: { build: {

View File

@ -87,10 +87,10 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
"@types/bootstrap@^5.2.5": "@types/bootstrap@^5.2.6":
version "5.2.5" version "5.2.6"
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.5.tgz#0bb5dea7720611b2bb7ba16bd8a64fafd86fb658" resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.6.tgz#e861b3aa1f4a1434da0bf50fbaa372b6f7e64d2f"
integrity sha512-VnalUJ3E/oaV3DYrauEc/sSPpaEPxTV09twSEzY4KFRvyuGlrZUSqG95XZ6ReAi0YMZIs7rXxdngDK2X1YONQA== integrity sha512-BlAc3YATdasbHoxMoBWODrSF6qwQO/E9X8wVxCCSa6rWjnaZfpkr2N6pUMCY6jj2+wf0muUtLySbvU9etX6YqA==
dependencies: dependencies:
"@popperjs/core" "^2.9.2" "@popperjs/core" "^2.9.2"
@ -99,10 +99,10 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/node@^18.11.8": "@types/node@^18.11.9":
version "18.11.8" version "18.11.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A== integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/spark-md5@^3.0.2": "@types/spark-md5@^3.0.2":
version "3.0.2" version "3.0.2"
@ -283,6 +283,16 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map "^0.6.1" 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": "@vue/compiler-dom@3.2.41", "@vue/compiler-dom@^3.2.40":
version "3.2.41" version "3.2.41"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz#dc63dcd3ce8ca8a8721f14009d498a7a54380299" 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/compiler-core" "3.2.41"
"@vue/shared" "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" version "3.2.41"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz#238fb8c48318408c856748f4116aff8cc1dc2a73" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz#238fb8c48318408c856748f4116aff8cc1dc2a73"
integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w== integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==
@ -315,6 +349,14 @@
"@vue/compiler-dom" "3.2.41" "@vue/compiler-dom" "3.2.41"
"@vue/shared" "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": "@vue/devtools-api@^6.4.5":
version "6.4.5" version "6.4.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380"
@ -340,43 +382,66 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.25.7" 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" version "3.2.41"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.41.tgz#0ad3bdf76d76822da1502dc9f394dafd02642963" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.41.tgz#0ad3bdf76d76822da1502dc9f394dafd02642963"
integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g== integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g==
dependencies: dependencies:
"@vue/shared" "3.2.41" "@vue/shared" "3.2.41"
"@vue/runtime-core@3.2.41": "@vue/runtime-core@3.2.45":
version "3.2.41" version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.41.tgz#775bfc00b3fadbaddab77138f23322aee3517a76" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz#7ad7ef9b2519d41062a30c6fa001ec43ac549c7f"
integrity sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ== integrity sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==
dependencies: dependencies:
"@vue/reactivity" "3.2.41" "@vue/reactivity" "3.2.45"
"@vue/shared" "3.2.41" "@vue/shared" "3.2.45"
"@vue/runtime-dom@3.2.41": "@vue/runtime-dom@3.2.45":
version "3.2.41" version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz#cdf86be7410f7b15c29632a96ce879e5b4c9ab92" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz#1a2ef6ee2ad876206fbbe2a884554bba2d0faf59"
integrity sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA== integrity sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==
dependencies: dependencies:
"@vue/runtime-core" "3.2.41" "@vue/runtime-core" "3.2.45"
"@vue/shared" "3.2.41" "@vue/shared" "3.2.45"
csstype "^2.6.8" csstype "^2.6.8"
"@vue/server-renderer@3.2.41": "@vue/server-renderer@3.2.45":
version "3.2.41" version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.41.tgz#ca64552c05878f94e8d191ac439141c06c0fb2ad" resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz#ca9306a0c12b0530a1a250e44f4a0abac6b81f3f"
integrity sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig== integrity sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==
dependencies: dependencies:
"@vue/compiler-ssr" "3.2.41" "@vue/compiler-ssr" "3.2.45"
"@vue/shared" "3.2.41" "@vue/shared" "3.2.45"
"@vue/shared@3.2.41", "@vue/shared@^3.2.40": "@vue/shared@3.2.41", "@vue/shared@^3.2.40":
version "3.2.41" version "3.2.41"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.41.tgz#fbc95422df654ea64e8428eced96ba6ad555d2bb" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.41.tgz#fbc95422df654ea64e8428eced96ba6ad555d2bb"
integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw== 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": "@vue/tsconfig@^0.1.3":
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.1.3.tgz#4a61dbd29783d01ddab504276dcf0c2b6988654f" resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.1.3.tgz#4a61dbd29783d01ddab504276dcf0c2b6988654f"
@ -421,6 +486,14 @@ ansi-styles@^4.1.0:
dependencies: dependencies:
color-convert "^2.0.1" 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: argparse@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 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" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 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: boolbase@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -466,7 +544,7 @@ brace-expansion@^2.0.1:
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
braces@^3.0.2: braces@^3.0.2, braces@~3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
@ -503,6 +581,21 @@ chalk@^4.0.0, chalk@^4.1.2:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.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: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 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" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@^8.26.0: eslint@^8.27.0:
version "8.26.0" version "8.27.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.26.0.tgz#2bcc8836e6c424c4ac26a5674a70d44d84f2181d" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.27.0.tgz#d547e2f7239994ad1faa4bb5d84e5d809db7cf64"
integrity sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg== integrity sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==
dependencies: dependencies:
"@eslint/eslintrc" "^1.3.3" "@eslint/eslintrc" "^1.3.3"
"@humanwhocodes/config-array" "^0.11.6" "@humanwhocodes/config-array" "^0.11.6"
@ -1057,7 +1150,7 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2" call-bind "^1.0.2"
get-intrinsic "^1.1.1" get-intrinsic "^1.1.1"
glob-parent@^5.1.2: glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 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" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== 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: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -1215,6 +1313,13 @@ is-bigint@^1.0.1:
dependencies: dependencies:
has-bigints "^1.0.1" 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: is-boolean-object@^1.1.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" 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" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 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" version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@ -1434,6 +1539,11 @@ minimatch@^5.1.0:
dependencies: dependencies:
brace-expansion "^2.0.1" 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: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 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" semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1" 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: npm-run-all@^4.1.5:
version "4.1.5" version "4.1.5"
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" 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" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 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" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -1666,6 +1781,13 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2" normalize-package-data "^2.3.2"
path-type "^3.0.0" 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: regexp.prototype.flags@^1.4.3:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" 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" get-intrinsic "^1.1.3"
is-regex "^1.1.4" 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: "semver@2 || 3 || 4 || 5", semver@^5.5.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 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" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 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" version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 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" 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== integrity sha512-gjIG6iFWde32oRr/bK9CsfN+jdbura2y4GlDzeOiEm6py38ud8dXzFl9zwmHjOjZdr8XEgQ9TovzVGNzp47esw==
vite@^3.2.2: vite@^3.2.3:
version "3.2.2" version "3.2.3"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.2.tgz#280762bfaf47bcea1d12698427331c0009ac7c1f" resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.3.tgz#7a68d9ef73eff7ee6dc0718ad3507adfc86944a7"
integrity sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw== integrity sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==
dependencies: dependencies:
esbuild "^0.15.9" esbuild "^0.15.9"
postcss "^8.4.18" postcss "^8.4.18"
@ -2031,16 +2162,16 @@ vue-tsc@^1.0.9:
"@volar/vue-language-core" "1.0.9" "@volar/vue-language-core" "1.0.9"
"@volar/vue-typescript" "1.0.9" "@volar/vue-typescript" "1.0.9"
vue@^3.2.41: vue@^3.2.45:
version "3.2.41" version "3.2.45"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.41.tgz#ed452b8a0f7f2b962f055c8955139c28b1c06806" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8"
integrity sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ== integrity sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==
dependencies: dependencies:
"@vue/compiler-dom" "3.2.41" "@vue/compiler-dom" "3.2.45"
"@vue/compiler-sfc" "3.2.41" "@vue/compiler-sfc" "3.2.45"
"@vue/runtime-dom" "3.2.41" "@vue/runtime-dom" "3.2.45"
"@vue/server-renderer" "3.2.41" "@vue/server-renderer" "3.2.45"
"@vue/shared" "3.2.41" "@vue/shared" "3.2.45"
which-boxed-primitive@^1.0.2: which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"

Binary file not shown.