Merge remote-tracking branch 'tbnobody/OpenDTU/master'
This commit is contained in:
commit
f35395e76f
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
- name: Get default environments
|
||||
id: envs
|
||||
run: |
|
||||
echo "::set-output name=environments::$(pio project config --json-output | jq -cr '.[0][1][0][1]')"
|
||||
echo "environments=$(pio project config --json-output | jq -cr '.[0][1][0][1]')" >> $GITHUB_OUTPUT
|
||||
|
||||
outputs:
|
||||
environments: ${{ steps.envs.outputs.environments }}
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"C_Cpp.clang_format_style": "WebKit"
|
||||
}
|
||||
@ -75,6 +75,7 @@ Sends text raw data as difined in VE.Direct spec.
|
||||
* Time zone support
|
||||
* Ve.Direct interface (via web-interface, REST-api, or MQTT)
|
||||
* Ethernet support
|
||||
* Prometheus API endpoint (/api/prometheus/metrics)
|
||||
|
||||
## Features for developers
|
||||
* The microcontroller part
|
||||
@ -206,7 +207,7 @@ Users report that [ESP_Flasher](https://github.com/Jason2866/ESP_Flasher/release
|
||||
## First configuration
|
||||
* After the initial flashing of the microcontroller, an Access Point called "OpenDTU-*" is opened. The default password is "openDTU42".
|
||||
* Use a web browser to open the address [http://192.168.4.1](http://192.168.4.1)
|
||||
* Navigate to Settings --> Network Settings and enter your WiFi credentials
|
||||
* Navigate to Settings --> Network Settings and enter your WiFi credentials. The username to access the config menu is "admin" and the password the same as for accessing the Access Point (default: "openDTU42").
|
||||
* OpenDTU then simultaneously connects to your WiFi AP with this credentials. Navigate to Info --> Network and look into section "Network Interface (Station)" for the IP address received via DHCP.
|
||||
* When OpenDTU is connected to a configured WiFI AP, the "OpenDTU-*" Access Point is closed after 3 minutes.
|
||||
* OpenDTU needs access to a working NTP server to get the current date & time. Both are sent to the inverter with each request. Default NTP server is pool.ntp.org. If your network has different requirements please change accordingly (Settings --> NTP Settings).
|
||||
@ -225,6 +226,9 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw
|
||||
## MQTT Topic Documentation
|
||||
A documentation of all available MQTT Topics can be found here: [MQTT Documentation](docs/MQTT_Topics.md)
|
||||
|
||||
## Web API Documentation
|
||||
A documentation of the Web API can be found here: [Web-API Documentation](docs/Web-API.md)
|
||||
|
||||
## Available cases
|
||||
* <https://www.thingiverse.com/thing:5435911>
|
||||
* <https://www.printables.com/model/293003-sol-opendtu-esp32-nrf24l01-case>
|
||||
|
||||
6
docs/README.md
Normal file
6
docs/README.md
Normal 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
460
docs/Web-API.md
Normal 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"}}
|
||||
```
|
||||
@ -12,6 +12,7 @@
|
||||
#include "WebApi_network.h"
|
||||
#include "WebApi_ntp.h"
|
||||
#include "WebApi_power.h"
|
||||
#include "WebApi_prometheus.h"
|
||||
#include "WebApi_security.h"
|
||||
#include "WebApi_sysstatus.h"
|
||||
#include "WebApi_webapp.h"
|
||||
@ -26,6 +27,8 @@ public:
|
||||
void init();
|
||||
void loop();
|
||||
|
||||
static bool checkCredentials(AsyncWebServerRequest* request);
|
||||
|
||||
private:
|
||||
AsyncWebServer _server;
|
||||
AsyncEventSource _events;
|
||||
@ -41,6 +44,7 @@ private:
|
||||
WebApiNetworkClass _webApiNetwork;
|
||||
WebApiNtpClass _webApiNtp;
|
||||
WebApiPowerClass _webApiPower;
|
||||
WebApiPrometheusClass _webApiPrometheus;
|
||||
WebApiSecurityClass _webApiSecurity;
|
||||
WebApiSysstatusClass _webApiSysstatus;
|
||||
WebApiWebappClass _webApiWebapp;
|
||||
|
||||
17
include/WebApi_prometheus.h
Normal file
17
include/WebApi_prometheus.h
Normal 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;
|
||||
};
|
||||
@ -12,5 +12,7 @@ private:
|
||||
void onPasswordGet(AsyncWebServerRequest* request);
|
||||
void onPasswordPost(AsyncWebServerRequest* request);
|
||||
|
||||
void onAuthenticateGet(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
#define ACCESS_POINT_NAME "OpenDTU-"
|
||||
#define ACCESS_POINT_PASSWORD "openDTU42"
|
||||
#define AUTH_USERNAME "admin"
|
||||
|
||||
#define ADMIN_TIMEOUT 180
|
||||
#define WIFI_RECONNECT_TIMEOUT 15
|
||||
|
||||
@ -4,10 +4,16 @@
|
||||
#include "inverters/HM_4CH.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#define HOY_SEMAPHORE_TAKE() xSemaphoreTake(_xSemaphore, portMAX_DELAY)
|
||||
#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore)
|
||||
|
||||
HoymilesClass Hoymiles;
|
||||
|
||||
void HoymilesClass::init()
|
||||
{
|
||||
_xSemaphore = xSemaphoreCreateMutex();
|
||||
HOY_SEMAPHORE_GIVE(); // release before first use
|
||||
|
||||
_pollInterval = 0;
|
||||
_radio.reset(new HoymilesRadio());
|
||||
_radio->init();
|
||||
@ -15,49 +21,51 @@ void HoymilesClass::init()
|
||||
|
||||
void HoymilesClass::loop()
|
||||
{
|
||||
HOY_SEMAPHORE_TAKE();
|
||||
_radio->loop();
|
||||
|
||||
if (getNumInverters() > 0) {
|
||||
if (millis() - _lastPoll > (_pollInterval * 1000)) {
|
||||
static uint8_t inverterPos = 0;
|
||||
|
||||
std::shared_ptr<InverterAbstract> iv = getInverterByPos(inverterPos);
|
||||
if (iv != nullptr && _radio->isIdle()) {
|
||||
Serial.print(F("Fetch inverter: "));
|
||||
Serial.println(iv->serial(), HEX);
|
||||
if (_radio->isIdle()) {
|
||||
std::shared_ptr<InverterAbstract> iv = getInverterByPos(inverterPos);
|
||||
if (iv != nullptr) {
|
||||
Serial.print(F("Fetch inverter: "));
|
||||
Serial.println(iv->serial(), HEX);
|
||||
|
||||
iv->sendStatsRequest(_radio.get());
|
||||
iv->sendStatsRequest(_radio.get());
|
||||
|
||||
// Fetch event log
|
||||
bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK;
|
||||
iv->sendAlarmLogRequest(_radio.get(), force);
|
||||
// Fetch event log
|
||||
bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK;
|
||||
iv->sendAlarmLogRequest(_radio.get(), force);
|
||||
|
||||
// Fetch limit
|
||||
if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK)
|
||||
|| ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL)
|
||||
&& (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) {
|
||||
Serial.println("Request SystemConfigPara");
|
||||
iv->sendSystemConfigParaRequest(_radio.get());
|
||||
// Fetch limit
|
||||
if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK)
|
||||
|| ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL)
|
||||
&& (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) {
|
||||
Serial.println("Request SystemConfigPara");
|
||||
iv->sendSystemConfigParaRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Set limit if required
|
||||
if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) {
|
||||
Serial.println(F("Resend ActivePowerControl"));
|
||||
iv->resendActivePowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Set power status if required
|
||||
if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) {
|
||||
Serial.println(F("Resend PowerCommand"));
|
||||
iv->resendPowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Fetch dev info (but first fetch stats)
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) {
|
||||
Serial.println(F("Request device info"));
|
||||
iv->sendDevInfoRequest(_radio.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Set limit if required
|
||||
if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) {
|
||||
Serial.println(F("Resend ActivePowerControl"));
|
||||
iv->resendActivePowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Set power status if required
|
||||
if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) {
|
||||
Serial.println(F("Resend PowerCommand"));
|
||||
iv->resendPowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Fetch dev info (but first fetch stats)
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) {
|
||||
Serial.println(F("Request device info"));
|
||||
iv->sendDevInfoRequest(_radio.get());
|
||||
}
|
||||
|
||||
if (++inverterPos >= getNumInverters()) {
|
||||
inverterPos = 0;
|
||||
}
|
||||
@ -66,6 +74,8 @@ void HoymilesClass::loop()
|
||||
_lastPoll = millis();
|
||||
}
|
||||
}
|
||||
|
||||
HOY_SEMAPHORE_GIVE();
|
||||
}
|
||||
|
||||
std::shared_ptr<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++) {
|
||||
if (_inverters[i]->serial() == serial) {
|
||||
HOY_SEMAPHORE_TAKE();
|
||||
_inverters.erase(_inverters.begin() + i);
|
||||
HOY_SEMAPHORE_GIVE();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ private:
|
||||
std::vector<std::shared_ptr<InverterAbstract>> _inverters;
|
||||
std::unique_ptr<HoymilesRadio> _radio;
|
||||
|
||||
SemaphoreHandle_t _xSemaphore;
|
||||
|
||||
uint32_t _pollInterval = 0;
|
||||
uint32_t _lastPoll = 0;
|
||||
};
|
||||
|
||||
@ -127,7 +127,7 @@ void HoymilesRadio::loop()
|
||||
}
|
||||
} else {
|
||||
// If inverter was not found, assume the command is invalid
|
||||
Serial.println(F("Invalid inverter found"));
|
||||
Serial.println(F("RX: Invalid inverter found"));
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
}
|
||||
@ -137,8 +137,13 @@ void HoymilesRadio::loop()
|
||||
CommandAbstract* cmd = _commandQueue.front().get();
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress());
|
||||
inv->clearRxFragmentBuffer();
|
||||
sendEsbPacket(cmd);
|
||||
if (nullptr != inv) {
|
||||
inv->clearRxFragmentBuffer();
|
||||
sendEsbPacket(cmd);
|
||||
} else {
|
||||
Serial.println(F("TX: Invalid inverter found"));
|
||||
_commandQueue.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ HM_Abstract::HM_Abstract(uint64_t serial)
|
||||
bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio)
|
||||
bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force)
|
||||
bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
|
||||
bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ build_flags =
|
||||
lib_deps =
|
||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||
bblanchon/ArduinoJson @ ^6.19.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.3.2
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.3.3
|
||||
nrf24/RF24 @ ^1.4.5
|
||||
|
||||
extra_scripts =
|
||||
|
||||
@ -152,7 +152,7 @@ void MqttHassPublishingClass::publishInverterButton(std::shared_ptr<InverterAbst
|
||||
String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = caption;
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + buttonId;
|
||||
if (strcmp(icon, "")) {
|
||||
root[F("ic")] = icon;
|
||||
@ -191,7 +191,7 @@ void MqttHassPublishingClass::publishInverterNumber(
|
||||
String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = caption;
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + buttonId;
|
||||
if (strcmp(icon, "")) {
|
||||
root[F("ic")] = icon;
|
||||
@ -226,7 +226,7 @@ void MqttHassPublishingClass::publishInverterBinarySensor(std::shared_ptr<Invert
|
||||
String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = caption;
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + sensorId;
|
||||
root[F("stat_t")] = statTopic;
|
||||
root[F("pl_on")] = payload_on;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "WebApi.h"
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiClass::WebApiClass()
|
||||
@ -28,6 +29,7 @@ void WebApiClass::init()
|
||||
_webApiNetwork.init(&_server);
|
||||
_webApiNtp.init(&_server);
|
||||
_webApiPower.init(&_server);
|
||||
_webApiPrometheus.init(&_server);
|
||||
_webApiSecurity.init(&_server);
|
||||
_webApiSysstatus.init(&_server);
|
||||
_webApiWebapp.init(&_server);
|
||||
@ -59,4 +61,22 @@ void WebApiClass::loop()
|
||||
_webApiVedirect.loop();
|
||||
}
|
||||
|
||||
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if (request->authenticate(AUTH_USERNAME, config.Security_Password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AsyncWebServerResponse* r = request->beginResponse(401);
|
||||
|
||||
// WebAPI should set the X-Requested-With to prevent browser internal auth dialogs
|
||||
if (!request->hasHeader("X-Requested-With")) {
|
||||
r->addHeader(F("WWW-Authenticate"), F("Basic realm=\"Login Required\""));
|
||||
}
|
||||
request->send(r);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
WebApiClass WebApi;
|
||||
@ -6,6 +6,7 @@
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "WebApi.h"
|
||||
#include <LittleFS.h>
|
||||
|
||||
void WebApiConfigClass::init(AsyncWebServer* server)
|
||||
@ -32,11 +33,19 @@ void WebApiConfigClass::loop()
|
||||
|
||||
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true);
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
@ -93,6 +102,10 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the request handler is triggered after the upload has finished...
|
||||
// create the response, add header, and send response
|
||||
|
||||
@ -108,6 +121,10 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!index) {
|
||||
// open the file on first call and store the file handle in the request object
|
||||
request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w");
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Hoymiles.h"
|
||||
#include "WebApi.h"
|
||||
|
||||
void WebApiDtuClass::init(AsyncWebServer* server)
|
||||
{
|
||||
@ -24,6 +25,10 @@ void WebApiDtuClass::loop()
|
||||
|
||||
void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
@ -43,6 +48,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Update.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiFirmwareClass::init(AsyncWebServer* server)
|
||||
@ -31,6 +32,10 @@ void WebApiFirmwareClass::loop()
|
||||
|
||||
void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the request handler is triggered after the upload has finished...
|
||||
// create the response, add header, and send response
|
||||
|
||||
@ -46,6 +51,10 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload handler chunks in data
|
||||
if (!index) {
|
||||
if (!request->hasParam("MD5", true)) {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "Configuration.h"
|
||||
#include "Hoymiles.h"
|
||||
#include "MqttHassPublishing.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiInverterClass::init(AsyncWebServer* server)
|
||||
@ -28,6 +29,10 @@ void WebApiInverterClass::loop()
|
||||
|
||||
void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U);
|
||||
JsonObject root = response->getRoot();
|
||||
JsonArray data = root.createNestedArray(F("inverter"));
|
||||
@ -48,13 +53,16 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
obj[F("serial")] = buffer;
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
|
||||
uint8_t max_channels;
|
||||
if (inv == nullptr) {
|
||||
obj[F("type")] = F("Unknown");
|
||||
max_channels = INV_MAX_CHAN_COUNT;
|
||||
} else {
|
||||
obj[F("type")] = inv->typeName();
|
||||
max_channels = inv->Statistics()->getChannelCount();
|
||||
}
|
||||
|
||||
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
|
||||
for (uint8_t c = 0; c < max_channels; c++) {
|
||||
obj[F("max_power")][c] = config.Inverter[i].MaxChannelPower[c];
|
||||
}
|
||||
}
|
||||
@ -66,6 +74,10 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
@ -151,6 +163,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
@ -210,7 +226,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
JsonArray maxPowerArray = root[F("max_power")].as<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!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -265,6 +281,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Hoymiles.h"
|
||||
#include "WebApi.h"
|
||||
|
||||
void WebApiLimitClass::init(AsyncWebServer* server)
|
||||
{
|
||||
@ -54,6 +55,10 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "Configuration.h"
|
||||
#include "MqttHassPublishing.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiMqttClass::init(AsyncWebServer* server)
|
||||
@ -54,6 +55,10 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
@ -83,6 +88,10 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiNetworkClass::init(AsyncWebServer* server)
|
||||
@ -52,6 +53,10 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
@ -72,6 +77,10 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "NtpSettings.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiNtpClass::init(AsyncWebServer* server)
|
||||
@ -37,7 +38,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
|
||||
root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr;
|
||||
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
root[F("ntp_status")] = false;
|
||||
} else {
|
||||
root[F("ntp_status")] = true;
|
||||
@ -52,6 +53,10 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
@ -66,6 +71,10 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
@ -142,11 +151,15 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 0)) {
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
root[F("ntp_status")] = false;
|
||||
} else {
|
||||
root[F("ntp_status")] = true;
|
||||
@ -165,6 +178,10 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Hoymiles.h"
|
||||
#include "WebApi.h"
|
||||
|
||||
void WebApiPowerClass::init(AsyncWebServer* server)
|
||||
{
|
||||
@ -47,6 +48,10 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
97
src/WebApi_prometheus.cpp
Normal file
97
src/WebApi_prometheus.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
void WebApiSecurityClass::init(AsyncWebServer* server)
|
||||
@ -16,6 +17,7 @@ void WebApiSecurityClass::init(AsyncWebServer* server)
|
||||
|
||||
_server->on("/api/security/password", HTTP_GET, std::bind(&WebApiSecurityClass::onPasswordGet, this, _1));
|
||||
_server->on("/api/security/password", HTTP_POST, std::bind(&WebApiSecurityClass::onPasswordPost, this, _1));
|
||||
_server->on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1));
|
||||
}
|
||||
|
||||
void WebApiSecurityClass::loop()
|
||||
@ -24,6 +26,10 @@ void WebApiSecurityClass::loop()
|
||||
|
||||
void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
@ -36,6 +42,10 @@ void WebApiSecurityClass::onPasswordGet(AsyncWebServerRequest* request)
|
||||
|
||||
void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
@ -90,3 +100,18 @@ void WebApiSecurityClass::onPasswordPost(AsyncWebServerRequest* request)
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Authentication successfull!");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
@ -14,23 +14,25 @@
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.2",
|
||||
"bootstrap-icons-vue": "^1.8.1",
|
||||
"mitt": "^3.0.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.2.41",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/bootstrap": "^5.2.5",
|
||||
"@types/node": "^18.11.8",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-vue": "^9.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.2",
|
||||
"vite": "^3.2.3",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-css-injected-by-js": "^2.1.1",
|
||||
"vue-tsc": "^1.0.9"
|
||||
|
||||
@ -14,7 +14,7 @@ import { defineComponent } from 'vue';
|
||||
import { formatNumber } from '@/utils';
|
||||
|
||||
declare interface LimitData {
|
||||
limit: number,
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" ref="navbarCollapse" id="navbarNavAltMarkup">
|
||||
<ul class="navbar-nav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<router-link @click="onClick" class="nav-link" to="/">Live Data</router-link>
|
||||
</li>
|
||||
@ -78,6 +78,10 @@
|
||||
<router-link @click="onClick" class="nav-link" to="/about">About</router-link>
|
||||
</li>
|
||||
</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>
|
||||
</nav>
|
||||
@ -85,13 +89,39 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { logout, isLoggedIn } from '@/utils/authentication';
|
||||
import { BIconSun } from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
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: {
|
||||
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() {
|
||||
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show");
|
||||
}
|
||||
|
||||
9
webapp/src/emitter.d.ts
vendored
Normal file
9
webapp/src/emitter.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import mitt from 'mitt';
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$emitter: Emitter;
|
||||
}
|
||||
}
|
||||
|
||||
export { } // Important! See note.
|
||||
@ -1,12 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import mitt from 'mitt';
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.min.css"
|
||||
import './scss/styles.scss'
|
||||
import "bootstrap"
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const emitter = mitt();
|
||||
app.config.globalProperties.$emitter = emitter;
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@ -15,6 +15,7 @@ import ConfigAdminView from '@/views/ConfigAdminView.vue'
|
||||
import VedirectAdminView from '@/views/VedirectAdminView.vue'
|
||||
import VedirectInfoView from '@/views/VedirectInfoView.vue'
|
||||
import SecurityAdminView from '@/views/SecurityAdminView.vue'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -25,6 +26,11 @@ const router = createRouter({
|
||||
name: 'Home',
|
||||
component: HomeView
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
@ -101,6 +107,22 @@ const router = createRouter({
|
||||
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;
|
||||
2
webapp/src/scss/styles.scss
Normal file
2
webapp/src/scss/styles.scss
Normal file
@ -0,0 +1,2 @@
|
||||
// Import all of Bootstrap's CSS
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@ -1,10 +1,10 @@
|
||||
export interface DevInfoStatus {
|
||||
valid_data: boolean,
|
||||
fw_bootloader_version: number,
|
||||
fw_build_version: number,
|
||||
fw_build_datetime: Date,
|
||||
hw_part_number: number,
|
||||
hw_version: number,
|
||||
hw_model_name: string,
|
||||
max_power: number,
|
||||
valid_data: boolean;
|
||||
fw_bootloader_version: number;
|
||||
fw_build_version: number;
|
||||
fw_build_datetime: Date;
|
||||
hw_part_number: number;
|
||||
hw_version: number;
|
||||
hw_model_name: string;
|
||||
max_power: number;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface DtuConfig {
|
||||
dtu_serial: number,
|
||||
dtu_pollinterval: number,
|
||||
dtu_palevel: number
|
||||
dtu_serial: number;
|
||||
dtu_pollinterval: number;
|
||||
dtu_palevel: number;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
export interface EventlogItem {
|
||||
message_id: number,
|
||||
message: string,
|
||||
start_time: number,
|
||||
end_time: number
|
||||
message_id: number;
|
||||
message: string;
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
}
|
||||
|
||||
export interface EventlogItems {
|
||||
count: number,
|
||||
events: Array<EventlogItem>,
|
||||
count: number;
|
||||
events: Array<EventlogItem>;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface LimitConfig {
|
||||
serial: number,
|
||||
limit_value: number,
|
||||
limit_type: number
|
||||
serial: number;
|
||||
limit_value: number;
|
||||
limit_type: number;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface LimitStatus {
|
||||
limit_relative: number,
|
||||
max_power: number,
|
||||
limit_set_status: string,
|
||||
limit_relative: number;
|
||||
max_power: number;
|
||||
limit_set_status: string;
|
||||
}
|
||||
@ -1,43 +1,43 @@
|
||||
export interface ValueObject {
|
||||
v: number, // value
|
||||
u: string, // unit
|
||||
d: number, // digits
|
||||
v: number; // value
|
||||
u: string; // unit
|
||||
d: number; // digits
|
||||
};
|
||||
|
||||
export interface InverterStatistics {
|
||||
Power?: ValueObject,
|
||||
Voltage?: ValueObject,
|
||||
Current?: ValueObject,
|
||||
"Power DC"?: ValueObject,
|
||||
YieldDay?: ValueObject,
|
||||
YieldTotal?: ValueObject,
|
||||
Frequency?: ValueObject,
|
||||
Temperature?: ValueObject,
|
||||
PowerFactor?: ValueObject,
|
||||
ReactivePower?: ValueObject,
|
||||
Efficiency?: ValueObject,
|
||||
Irradiation?: ValueObject,
|
||||
Power?: ValueObject;
|
||||
Voltage?: ValueObject;
|
||||
Current?: ValueObject;
|
||||
"Power DC"?: ValueObject;
|
||||
YieldDay?: ValueObject;
|
||||
YieldTotal?: ValueObject;
|
||||
Frequency?: ValueObject;
|
||||
Temperature?: ValueObject;
|
||||
PowerFactor?: ValueObject;
|
||||
ReactivePower?: ValueObject;
|
||||
Efficiency?: ValueObject;
|
||||
Irradiation?: ValueObject;
|
||||
}
|
||||
|
||||
export interface Inverter {
|
||||
serial: number,
|
||||
name: string,
|
||||
data_age: number,
|
||||
reachable: boolean,
|
||||
producing: boolean,
|
||||
limit_relative: number,
|
||||
limit_absolute: number,
|
||||
events: number,
|
||||
[key: number]: InverterStatistics,
|
||||
serial: number;
|
||||
name: string;
|
||||
data_age: number;
|
||||
reachable: boolean;
|
||||
producing: boolean;
|
||||
limit_relative: number;
|
||||
limit_absolute: number;
|
||||
events: number;
|
||||
[key: number]: InverterStatistics;
|
||||
};
|
||||
|
||||
export interface Total {
|
||||
Power: ValueObject,
|
||||
YieldDay: ValueObject,
|
||||
YieldTotal: ValueObject,
|
||||
Power: ValueObject;
|
||||
YieldDay: ValueObject;
|
||||
YieldTotal: ValueObject;
|
||||
};
|
||||
|
||||
export interface LiveData {
|
||||
inverters: Inverter[],
|
||||
total: Total,
|
||||
inverters: Inverter[];
|
||||
total: Total;
|
||||
}
|
||||
@ -1,20 +1,20 @@
|
||||
export interface MqttConfig {
|
||||
mqtt_enabled: boolean,
|
||||
mqtt_hostname: string,
|
||||
mqtt_port: number,
|
||||
mqtt_username: string,
|
||||
mqtt_password: string,
|
||||
mqtt_topic: string,
|
||||
mqtt_publish_interval: number,
|
||||
mqtt_retain: boolean,
|
||||
mqtt_tls: boolean,
|
||||
mqtt_root_ca_cert: string,
|
||||
mqtt_lwt_topic: string,
|
||||
mqtt_lwt_online: string,
|
||||
mqtt_lwt_offline: string,
|
||||
mqtt_hass_enabled: boolean,
|
||||
mqtt_hass_expire: boolean,
|
||||
mqtt_hass_retain: boolean,
|
||||
mqtt_hass_topic: string,
|
||||
mqtt_hass_individualpanels: boolean
|
||||
mqtt_enabled: boolean;
|
||||
mqtt_hostname: string;
|
||||
mqtt_port: number;
|
||||
mqtt_username: string;
|
||||
mqtt_password: string;
|
||||
mqtt_topic: string;
|
||||
mqtt_publish_interval: number;
|
||||
mqtt_retain: boolean;
|
||||
mqtt_tls: boolean;
|
||||
mqtt_root_ca_cert: string;
|
||||
mqtt_lwt_topic: string;
|
||||
mqtt_lwt_online: string;
|
||||
mqtt_lwt_offline: string;
|
||||
mqtt_hass_enabled: boolean;
|
||||
mqtt_hass_expire: boolean;
|
||||
mqtt_hass_retain: boolean;
|
||||
mqtt_hass_topic: string;
|
||||
mqtt_hass_individualpanels: boolean;
|
||||
}
|
||||
@ -1,17 +1,17 @@
|
||||
export interface MqttStatus {
|
||||
mqtt_enabled: boolean,
|
||||
mqtt_hostname: string,
|
||||
mqtt_port: number,
|
||||
mqtt_username: string,
|
||||
mqtt_topic: string,
|
||||
mqtt_publish_interval: number,
|
||||
mqtt_retain: boolean,
|
||||
mqtt_tls: boolean,
|
||||
mqtt_root_ca_cert_info: string,
|
||||
mqtt_connected: boolean,
|
||||
mqtt_hass_enabled: boolean,
|
||||
mqtt_hass_expire: boolean,
|
||||
mqtt_hass_retain: boolean,
|
||||
mqtt_hass_topic: string,
|
||||
mqtt_hass_individualpanels: boolean
|
||||
mqtt_enabled: boolean;
|
||||
mqtt_hostname: string;
|
||||
mqtt_port: number;
|
||||
mqtt_username: string;
|
||||
mqtt_topic: string;
|
||||
mqtt_publish_interval: number;
|
||||
mqtt_retain: boolean;
|
||||
mqtt_tls: boolean;
|
||||
mqtt_root_ca_cert_info: string;
|
||||
mqtt_connected: boolean;
|
||||
mqtt_hass_enabled: boolean;
|
||||
mqtt_hass_expire: boolean;
|
||||
mqtt_hass_retain: boolean;
|
||||
mqtt_hass_topic: string;
|
||||
mqtt_hass_individualpanels: boolean;
|
||||
}
|
||||
@ -1,22 +1,22 @@
|
||||
export interface NetworkStatus {
|
||||
// WifiStationInfo
|
||||
sta_status: boolean,
|
||||
sta_ssid: string,
|
||||
sta_rssi: number,
|
||||
sta_status: boolean;
|
||||
sta_ssid: string;
|
||||
sta_rssi: number;
|
||||
// WifiApInfo
|
||||
ap_status: boolean,
|
||||
ap_ssid: string,
|
||||
ap_stationnum: number,
|
||||
ap_status: boolean;
|
||||
ap_ssid: string;
|
||||
ap_stationnum: number;
|
||||
// InterfaceNetworkInfo
|
||||
network_hostname: string,
|
||||
network_ip: string,
|
||||
network_netmask: string,
|
||||
network_gateway: string,
|
||||
network_dns1: string,
|
||||
network_dns2: string,
|
||||
network_mac: string,
|
||||
network_mode: string,
|
||||
network_hostname: string;
|
||||
network_ip: string;
|
||||
network_netmask: string;
|
||||
network_gateway: string;
|
||||
network_dns1: string;
|
||||
network_dns2: string;
|
||||
network_mac: string;
|
||||
network_mode: string;
|
||||
// InterfaceApInfo
|
||||
ap_ip: string,
|
||||
ap_mac: string,
|
||||
ap_ip: string;
|
||||
ap_mac: string;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
export interface NetworkConfig {
|
||||
ssid: string,
|
||||
password: string,
|
||||
hostname: string,
|
||||
dhcp: boolean,
|
||||
ipaddress: string,
|
||||
netmask: string,
|
||||
gateway: string,
|
||||
dns1: string,
|
||||
dns2: string
|
||||
ssid: string;
|
||||
password: string;
|
||||
hostname: string;
|
||||
dhcp: boolean;
|
||||
ipaddress: string;
|
||||
netmask: string;
|
||||
gateway: string;
|
||||
dns1: string;
|
||||
dns2: string;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface NtpConfig {
|
||||
ntp_server: string,
|
||||
ntp_timezone: string,
|
||||
ntp_timezone_descr: string
|
||||
ntp_server: string;
|
||||
ntp_timezone: string;
|
||||
ntp_timezone_descr: string;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
export interface NtpStatus {
|
||||
ntp_server: string,
|
||||
ntp_timezone: string,
|
||||
ntp_server: string;
|
||||
ntp_timezone: string;
|
||||
ntp_timezone_descr: string
|
||||
ntp_status: boolean,
|
||||
ntp_localtime: string
|
||||
ntp_status: boolean;
|
||||
ntp_localtime: string;
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
export interface SecurityConfig {
|
||||
password: string
|
||||
password: string;
|
||||
}
|
||||
@ -1,29 +1,29 @@
|
||||
export interface SystemStatus {
|
||||
// HardwareInfo
|
||||
chipmodel: string,
|
||||
chiprevision: number,
|
||||
chipcores: number,
|
||||
cpufreq: number,
|
||||
chipmodel: string;
|
||||
chiprevision: number;
|
||||
chipcores: number;
|
||||
cpufreq: number;
|
||||
// FirmwareInfo
|
||||
hostname: string,
|
||||
sdkversion: string,
|
||||
config_version: string,
|
||||
git_hash: string,
|
||||
resetreason_0: string,
|
||||
resetreason_1: string,
|
||||
cfgsavecount: number,
|
||||
uptime: number,
|
||||
update_text: string,
|
||||
update_url: string,
|
||||
update_status: string,
|
||||
hostname: string;
|
||||
sdkversion: string;
|
||||
config_version: string;
|
||||
git_hash: string;
|
||||
resetreason_0: string;
|
||||
resetreason_1: string;
|
||||
cfgsavecount: number;
|
||||
uptime: number;
|
||||
update_text: string;
|
||||
update_url: string;
|
||||
update_status: string;
|
||||
// MemoryInfo
|
||||
heap_total: number,
|
||||
heap_used: number,
|
||||
littlefs_total: number,
|
||||
littlefs_used: number,
|
||||
sketch_total: number,
|
||||
sketch_used: number,
|
||||
heap_total: number;
|
||||
heap_used: number;
|
||||
littlefs_total: number;
|
||||
littlefs_used: number;
|
||||
sketch_total: number;
|
||||
sketch_used: number;
|
||||
// RadioInfo
|
||||
radio_connected: boolean,
|
||||
radio_pvariant: boolean,
|
||||
radio_connected: boolean;
|
||||
radio_pvariant: boolean;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
export interface VedirectConfig {
|
||||
vedirect_enabled: boolean,
|
||||
vedirect_pollinterval: number,
|
||||
vedirect_updatesonly: boolean
|
||||
vedirect_enabled: boolean;
|
||||
vedirect_pollinterval: number;
|
||||
vedirect_updatesonly: boolean;
|
||||
}
|
||||
@ -2,24 +2,24 @@ import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
// Ve.Direct
|
||||
export interface Vedirect {
|
||||
SER: string,
|
||||
PID: string,
|
||||
FW: string,
|
||||
age_critical: boolean,
|
||||
data_age: 0,
|
||||
LOAD: ValueObject,
|
||||
CS: ValueObject,
|
||||
MPPT: ValueObject,
|
||||
OR: ValueObject,
|
||||
ERR: ValueObject,
|
||||
HSDS: ValueObject,
|
||||
V: ValueObject,
|
||||
I: ValueObject,
|
||||
VPV: ValueObject,
|
||||
PPV: ValueObject,
|
||||
H19: ValueObject,
|
||||
H20: ValueObject,
|
||||
H21: ValueObject,
|
||||
H22: ValueObject,
|
||||
H23: ValueObject,
|
||||
SER: string;
|
||||
PID: string;
|
||||
FW: string;
|
||||
age_critical: boolean;
|
||||
data_age: 0;
|
||||
LOAD: ValueObject;
|
||||
CS: ValueObject;
|
||||
MPPT: ValueObject;
|
||||
OR: ValueObject;
|
||||
ERR: ValueObject;
|
||||
HSDS: ValueObject;
|
||||
V: ValueObject;
|
||||
I: ValueObject;
|
||||
VPV: ValueObject;
|
||||
PPV: ValueObject;
|
||||
H19: ValueObject;
|
||||
H20: ValueObject;
|
||||
H21: ValueObject;
|
||||
H22: ValueObject;
|
||||
H23: ValueObject;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export interface VedirectStatus {
|
||||
vedirect_enabled: boolean,
|
||||
vedirect_updatesonly: boolean
|
||||
vedirect_enabled: boolean;
|
||||
vedirect_updatesonly: boolean;
|
||||
}
|
||||
84
webapp/src/utils/authentication.ts
Normal file
84
webapp/src/utils/authentication.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
@ -1,12 +1,19 @@
|
||||
import { timestampToString } from './time';
|
||||
import { formatNumber } from './number';
|
||||
import { login, logout, isLoggedIn } from './authentication';
|
||||
|
||||
export {
|
||||
timestampToString,
|
||||
formatNumber,
|
||||
login,
|
||||
logout,
|
||||
isLoggedIn,
|
||||
};
|
||||
|
||||
export default {
|
||||
timestampToString,
|
||||
formatNumber,
|
||||
login,
|
||||
logout,
|
||||
isLoggedIn,
|
||||
}
|
||||
@ -112,6 +112,7 @@ import {
|
||||
} from 'bootstrap-icons-vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -152,15 +153,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/config/delete", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
@ -171,10 +167,17 @@ export default defineComponent({
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
downloadConfig() {
|
||||
const link = document.createElement('a')
|
||||
link.href = "/api/config/get"
|
||||
link.download = 'config.json'
|
||||
link.click()
|
||||
fetch("/api/config/get", { headers: authHeader() })
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
var file = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = file;
|
||||
a.download = "config.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
});
|
||||
},
|
||||
uploadConfig(event: Event | null) {
|
||||
this.uploading = true;
|
||||
@ -206,6 +209,9 @@ export default defineComponent({
|
||||
|
||||
formData.append("config", this.file, "config");
|
||||
request.open("post", "/api/config/upload");
|
||||
authHeader().forEach((value, key) => {
|
||||
request.setRequestHeader(key, value);
|
||||
});
|
||||
request.send(formData);
|
||||
},
|
||||
clear() {
|
||||
|
||||
@ -49,6 +49,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { DtuConfig } from "@/types/DtuConfig";
|
||||
|
||||
export default defineComponent({
|
||||
@ -77,8 +78,8 @@ export default defineComponent({
|
||||
methods: {
|
||||
getDtuConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/dtu/config")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/dtu/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(data) => {
|
||||
this.dtuConfigList = data;
|
||||
@ -94,15 +95,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/dtu/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -77,6 +77,7 @@ import {
|
||||
BIconArrowRepeat,
|
||||
BIconCheckCircle
|
||||
} from 'bootstrap-icons-vue';
|
||||
import { authHeader } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -160,6 +161,9 @@ export default defineComponent({
|
||||
formData.append("MD5", (md5 as string));
|
||||
formData.append("firmware", this.file, "firmware");
|
||||
request.open("post", "/api/firmware/update");
|
||||
authHeader().forEach((value, key) => {
|
||||
request.setRequestHeader(key, value);
|
||||
});
|
||||
request.send(formData);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<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">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<BIconPower style="font-size:24px;" />
|
||||
|
||||
@ -340,6 +340,7 @@ import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
import type { LiveData, Inverter } from '@/types/LiveDataStatus';
|
||||
import type { LimitStatus } from '@/types/LimitStatus';
|
||||
import type { LimitConfig } from '@/types/LimitConfig';
|
||||
import { isLoggedIn, handleResponse, authHeader } from '@/utils/authentication';
|
||||
import { formatNumber } from '@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
@ -364,6 +365,8 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLogged: this.isLoggedIn(),
|
||||
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
@ -406,6 +409,12 @@ export default defineComponent({
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
this.$emitter.on("logged-in", () => {
|
||||
this.isLogged = this.isLoggedIn();
|
||||
});
|
||||
this.$emitter.on("logged-out", () => {
|
||||
this.isLogged = this.isLoggedIn();
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.eventLogView = new bootstrap.Modal('#eventView');
|
||||
@ -449,6 +458,7 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
formatNumber,
|
||||
isLoggedIn,
|
||||
getInitialData() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/livedata/status")
|
||||
@ -568,15 +578,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/limit/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
@ -643,15 +648,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/power/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BasePage :title="'Inverter Settings'" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
<BootstrapAlert v-model="alert.show" dismissible :variant="alert.type">
|
||||
{{ alert.message }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="card">
|
||||
@ -10,12 +10,12 @@
|
||||
<form class="form-inline" v-on:submit.prevent="onSubmit">
|
||||
<div class="form-group">
|
||||
<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 />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 />
|
||||
</div>
|
||||
<div class="ml-auto text-right">
|
||||
@ -44,22 +44,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="inverter in sortedInverters" v-bind:key="inverter.id">
|
||||
|
||||
<td>
|
||||
{{ inverter.serial }}
|
||||
</td>
|
||||
<td>
|
||||
{{ inverter.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ inverter.type }}
|
||||
</td>
|
||||
<td>{{ inverter.serial }}</td>
|
||||
<td>{{ inverter.name }}</td>
|
||||
<td>{{ inverter.type }}</td>
|
||||
<td>
|
||||
<a href="#" class="icon text-danger" title="Delete inverter">
|
||||
<BIconTrash v-on:click="onDeleteModal(inverter)" />
|
||||
<BIconTrash v-on:click="onOpenModal(modalDelete, inverter)" />
|
||||
</a>
|
||||
<a href="#" class="icon" title="Edit inverter">
|
||||
<BIconPencil v-on:click="onEdit(inverter)" />
|
||||
<BIconPencil v-on:click="onOpenModal(modal, inverter)" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -78,39 +71,36 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="inverter-serial" class="col-form-label">Serial:</label>
|
||||
<input v-model="editInverterData.serial" type="number" id="inverter-serial"
|
||||
class="form-control" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input v-model="selectedInverterData.serial" type="number" id="inverter-serial"
|
||||
class="form-control" />
|
||||
<label for="inverter-name" class="col-form-label">Name:</label>
|
||||
<input v-model="editInverterData.name" type="text" id="inverter-name" class="form-control"
|
||||
maxlength="31" />
|
||||
<input v-model="selectedInverterData.name" type="text" id="inverter-name"
|
||||
class="form-control" maxlength="31" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3" v-for="(max, index) in editInverterData.max_power" :key="`${index}`">
|
||||
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +
|
||||
1
|
||||
}}:</label>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
|
||||
v-model="editInverterData.max_power[index]"
|
||||
:aria-describedby="`inverter-maxDescription_${index} inverter-maxHelpText_${index}`" />
|
||||
<span class="input-group-text" :id="`inverter-maxDescription_${index}`">W</span>
|
||||
<div v-for="(max, index) in selectedInverterData.max_power" :key="`${index}`">
|
||||
<label :for="`inverter-max_${index}`" class="col-form-label">Max power string {{ index +1 }}:</label>
|
||||
<div class="d-flex mb-2">
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" :id="`inverter-max_${index}`" min="0"
|
||||
v-model="selectedInverterData.max_power[index]"
|
||||
:aria-describedby="`inverter-maxDescription_${index} inverter-customizer`" />
|
||||
<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 :id="`inverter-customizer`" class="form-text">*) Input the kWp of the channel to
|
||||
calculate irradiation.</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<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>
|
||||
<button type="button" class="btn btn-primary" @click="onEditSubmit(editId)">Save
|
||||
<button type="button" class="btn btn-primary" @click="onEditSubmit">Save
|
||||
changes</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -125,13 +115,13 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete the inverter "{{ deleteInverterData.name }}" with serial number
|
||||
{{ deleteInverterData.serial }}?
|
||||
Are you sure you want to delete the inverter "{{ selectedInverterData.name }}" with serial number
|
||||
{{ selectedInverterData.serial }}?
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
@ -147,13 +137,20 @@ import {
|
||||
} from 'bootstrap-icons-vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
|
||||
declare interface Inverter {
|
||||
id: string,
|
||||
serial: number,
|
||||
name: string,
|
||||
type: string
|
||||
max_power: number[]
|
||||
id: string;
|
||||
serial: number;
|
||||
name: string;
|
||||
type: string;
|
||||
max_power: number[];
|
||||
}
|
||||
|
||||
declare interface AlertResponse {
|
||||
message: string;
|
||||
type: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
@ -167,16 +164,11 @@ export default defineComponent({
|
||||
return {
|
||||
modal: {} as bootstrap.Modal,
|
||||
modalDelete: {} as bootstrap.Modal,
|
||||
deleteId: -1,
|
||||
editId: "-1",
|
||||
inverterData: {} as Inverter,
|
||||
editInverterData: {} as Inverter,
|
||||
deleteInverterData: {} as Inverter,
|
||||
newInverterData: {} as Inverter,
|
||||
selectedInverterData: {} as Inverter,
|
||||
inverters: [] as Inverter[],
|
||||
dataLoading: true,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
alert: {} as AlertResponse
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -196,130 +188,49 @@ export default defineComponent({
|
||||
methods: {
|
||||
getInverters() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/inverter/list")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/inverter/list", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then((data) => {
|
||||
this.inverters = data.inverter;
|
||||
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() {
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.inverterData));
|
||||
|
||||
fetch("/api/inverter/add", {
|
||||
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.inverterData.serial = 0;
|
||||
this.inverterData.name = "";
|
||||
this.callInverterApiEndpoint("add", JSON.stringify(this.newInverterData));
|
||||
this.newInverterData = {} as Inverter;
|
||||
},
|
||||
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;
|
||||
onDelete() {
|
||||
this.callInverterApiEndpoint("del", JSON.stringify({ id: this.selectedInverterData.id }));
|
||||
this.onCloseModal(this.modalDelete);
|
||||
},
|
||||
onDeleteCancel() {
|
||||
this.deleteId = -1;
|
||||
this.deleteInverterData.serial = 0;
|
||||
this.deleteInverterData.name = "";
|
||||
this.deleteInverterData.max_power = [];
|
||||
this.modalDelete.hide();
|
||||
onEditSubmit() {
|
||||
this.callInverterApiEndpoint("edit", JSON.stringify(this.selectedInverterData));
|
||||
this.onCloseModal(this.modal);
|
||||
},
|
||||
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();
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
89
webapp/src/views/LoginView.vue
Normal file
89
webapp/src/views/LoginView.vue
Normal 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>
|
||||
@ -218,6 +218,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { MqttConfig } from "@/types/MqttConfig";
|
||||
|
||||
export default defineComponent({
|
||||
@ -240,8 +241,8 @@ export default defineComponent({
|
||||
methods: {
|
||||
getMqttConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/mqtt/config")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/mqtt/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then((data) => {
|
||||
this.mqttConfigList = data;
|
||||
this.dataLoading = false;
|
||||
@ -255,15 +256,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/mqtt/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -104,6 +104,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { NetworkConfig } from "@/types/NetworkkConfig";
|
||||
|
||||
export default defineComponent({
|
||||
@ -126,8 +127,8 @@ export default defineComponent({
|
||||
methods: {
|
||||
getNetworkConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/network/config")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/network/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then((data) => {
|
||||
this.networkConfigList = data;
|
||||
this.dataLoading = false;
|
||||
@ -141,15 +142,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/network/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { NtpConfig } from "@/types/NtpConfig";
|
||||
|
||||
export default defineComponent({
|
||||
@ -127,8 +128,8 @@ export default defineComponent({
|
||||
},
|
||||
getNtpConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/ntp/config")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/ntp/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(data) => {
|
||||
this.ntpConfigList = data;
|
||||
@ -142,8 +143,8 @@ export default defineComponent({
|
||||
},
|
||||
getCurrentTime() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/ntp/time")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/ntp/time", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(data) => {
|
||||
this.mcuTime = new Date(
|
||||
@ -168,15 +169,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/ntp/time", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
@ -196,15 +192,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/ntp/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -26,8 +26,8 @@
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b>
|
||||
The administrator password is used to connect to the device when in AP mode.
|
||||
It must be 8..64 characters.
|
||||
The administrator password is used to access this web interface (user 'admin'), but also to
|
||||
connect to the device when in AP mode. It must be 8..64 characters.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -41,6 +41,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { SecurityConfig } from '@/types/SecurityConfig';
|
||||
|
||||
export default defineComponent({
|
||||
@ -65,8 +66,8 @@ export default defineComponent({
|
||||
methods: {
|
||||
getPasswordConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/security/password")
|
||||
.then((response) => response.json())
|
||||
fetch("/api/security/password", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(data) => {
|
||||
this.securityConfigList = data;
|
||||
@ -90,15 +91,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/security/password", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -51,6 +51,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { VedirectConfig } from "@/types/VedirectConfig";
|
||||
|
||||
export default defineComponent({
|
||||
@ -73,8 +74,8 @@ export default defineComponent({
|
||||
methods: {
|
||||
getVedirectConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/vedirect/config")
|
||||
.then((response) => response.json())
|
||||
fetch("api/vedirect/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then((data) => {
|
||||
this.vedirectConfigList = data;
|
||||
this.dataLoading = false;
|
||||
@ -88,15 +89,10 @@ export default defineComponent({
|
||||
|
||||
fetch("/api/vedirect/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.status != 200) {
|
||||
throw response.status;
|
||||
} else {
|
||||
return response.json();
|
||||
}
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = response.message;
|
||||
|
||||
@ -6,6 +6,8 @@ import vue from '@vitejs/plugin-vue'
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
@ -14,7 +16,8 @@ export default defineConfig({
|
||||
cssInjectedByJsPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
|
||||
}
|
||||
},
|
||||
build: {
|
||||
|
||||
231
webapp/yarn.lock
231
webapp/yarn.lock
@ -87,10 +87,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
|
||||
integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==
|
||||
|
||||
"@types/bootstrap@^5.2.5":
|
||||
version "5.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.5.tgz#0bb5dea7720611b2bb7ba16bd8a64fafd86fb658"
|
||||
integrity sha512-VnalUJ3E/oaV3DYrauEc/sSPpaEPxTV09twSEzY4KFRvyuGlrZUSqG95XZ6ReAi0YMZIs7rXxdngDK2X1YONQA==
|
||||
"@types/bootstrap@^5.2.6":
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.6.tgz#e861b3aa1f4a1434da0bf50fbaa372b6f7e64d2f"
|
||||
integrity sha512-BlAc3YATdasbHoxMoBWODrSF6qwQO/E9X8wVxCCSa6rWjnaZfpkr2N6pUMCY6jj2+wf0muUtLySbvU9etX6YqA==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
|
||||
@ -99,10 +99,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/node@^18.11.8":
|
||||
version "18.11.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265"
|
||||
integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A==
|
||||
"@types/node@^18.11.9":
|
||||
version "18.11.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
|
||||
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
|
||||
|
||||
"@types/spark-md5@^3.0.2":
|
||||
version "3.0.2"
|
||||
@ -283,6 +283,16 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-core@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b"
|
||||
integrity sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/shared" "3.2.45"
|
||||
estree-walker "^2.0.2"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-dom@3.2.41", "@vue/compiler-dom@^3.2.40":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz#dc63dcd3ce8ca8a8721f14009d498a7a54380299"
|
||||
@ -291,7 +301,31 @@
|
||||
"@vue/compiler-core" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
|
||||
"@vue/compiler-sfc@3.2.41", "@vue/compiler-sfc@^3.2.40":
|
||||
"@vue/compiler-dom@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz#c43cc15e50da62ecc16a42f2622d25dc5fd97dce"
|
||||
integrity sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
"@vue/compiler-sfc@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz#7f7989cc04ec9e7c55acd406827a2c4e96872c70"
|
||||
integrity sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/compiler-ssr" "3.2.45"
|
||||
"@vue/reactivity-transform" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
postcss "^8.1.10"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-sfc@^3.2.40":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz#238fb8c48318408c856748f4116aff8cc1dc2a73"
|
||||
integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w==
|
||||
@ -315,6 +349,14 @@
|
||||
"@vue/compiler-dom" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
|
||||
"@vue/compiler-ssr@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz#bd20604b6e64ea15344d5b6278c4141191c983b2"
|
||||
integrity sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
"@vue/devtools-api@^6.4.5":
|
||||
version "6.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380"
|
||||
@ -340,43 +382,66 @@
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
|
||||
"@vue/reactivity@3.2.41", "@vue/reactivity@^3.2.40":
|
||||
"@vue/reactivity-transform@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz#07ac83b8138550c83dfb50db43cde1e0e5e8124d"
|
||||
integrity sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
|
||||
"@vue/reactivity@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.45.tgz#412a45b574de601be5a4a5d9a8cbd4dee4662ff0"
|
||||
integrity sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==
|
||||
dependencies:
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
"@vue/reactivity@^3.2.40":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.41.tgz#0ad3bdf76d76822da1502dc9f394dafd02642963"
|
||||
integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g==
|
||||
dependencies:
|
||||
"@vue/shared" "3.2.41"
|
||||
|
||||
"@vue/runtime-core@3.2.41":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.41.tgz#775bfc00b3fadbaddab77138f23322aee3517a76"
|
||||
integrity sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ==
|
||||
"@vue/runtime-core@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz#7ad7ef9b2519d41062a30c6fa001ec43ac549c7f"
|
||||
integrity sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
"@vue/reactivity" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
"@vue/runtime-dom@3.2.41":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz#cdf86be7410f7b15c29632a96ce879e5b4c9ab92"
|
||||
integrity sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA==
|
||||
"@vue/runtime-dom@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz#1a2ef6ee2ad876206fbbe2a884554bba2d0faf59"
|
||||
integrity sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
"@vue/runtime-core" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
csstype "^2.6.8"
|
||||
|
||||
"@vue/server-renderer@3.2.41":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.41.tgz#ca64552c05878f94e8d191ac439141c06c0fb2ad"
|
||||
integrity sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig==
|
||||
"@vue/server-renderer@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz#ca9306a0c12b0530a1a250e44f4a0abac6b81f3f"
|
||||
integrity sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
"@vue/compiler-ssr" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
"@vue/shared@3.2.41", "@vue/shared@^3.2.40":
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.41.tgz#fbc95422df654ea64e8428eced96ba6ad555d2bb"
|
||||
integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==
|
||||
|
||||
"@vue/shared@3.2.45":
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2"
|
||||
integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==
|
||||
|
||||
"@vue/tsconfig@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.1.3.tgz#4a61dbd29783d01ddab504276dcf0c2b6988654f"
|
||||
@ -421,6 +486,14 @@ ansi-styles@^4.1.0:
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
anymatch@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
|
||||
dependencies:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
@ -436,6 +509,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
boolbase@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
@ -466,7 +544,7 @@ brace-expansion@^2.0.1:
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.2:
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
@ -503,6 +581,21 @@ chalk@^4.0.0, chalk@^4.1.2:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
"chokidar@>=3.0.0 <4.0.0":
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||
dependencies:
|
||||
anymatch "~3.1.2"
|
||||
braces "~3.0.2"
|
||||
glob-parent "~5.1.2"
|
||||
is-binary-path "~2.1.0"
|
||||
is-glob "~4.0.1"
|
||||
normalize-path "~3.0.0"
|
||||
readdirp "~3.6.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@ -831,10 +924,10 @@ eslint-visitor-keys@^3.3.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@^8.26.0:
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.26.0.tgz#2bcc8836e6c424c4ac26a5674a70d44d84f2181d"
|
||||
integrity sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==
|
||||
eslint@^8.27.0:
|
||||
version "8.27.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.27.0.tgz#d547e2f7239994ad1faa4bb5d84e5d809db7cf64"
|
||||
integrity sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.3"
|
||||
"@humanwhocodes/config-array" "^0.11.6"
|
||||
@ -1057,7 +1150,7 @@ get-symbol-description@^1.0.0:
|
||||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.1"
|
||||
|
||||
glob-parent@^5.1.2:
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
@ -1168,6 +1261,11 @@ ignore@^5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
|
||||
|
||||
immutable@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
|
||||
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
|
||||
|
||||
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@ -1215,6 +1313,13 @@ is-bigint@^1.0.1:
|
||||
dependencies:
|
||||
has-bigints "^1.0.1"
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-boolean-object@^1.1.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
|
||||
@ -1247,7 +1352,7 @@ is-extglob@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
@ -1434,6 +1539,11 @@ minimatch@^5.1.0:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
mitt@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
|
||||
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
@ -1469,6 +1579,11 @@ normalize-package-data@^2.3.2:
|
||||
semver "2 || 3 || 4 || 5"
|
||||
validate-npm-package-license "^3.0.1"
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
npm-run-all@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba"
|
||||
@ -1601,7 +1716,7 @@ picocolors@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
picomatch@^2.3.1:
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
@ -1666,6 +1781,13 @@ read-pkg@^3.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^3.0.0"
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
regexp.prototype.flags@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
|
||||
@ -1729,6 +1851,15 @@ safe-regex-test@^1.0.0:
|
||||
get-intrinsic "^1.1.3"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
sass@^1.56.1:
|
||||
version "1.56.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7"
|
||||
integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
@ -1784,7 +1915,7 @@ slash@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
@ -1983,10 +2114,10 @@ vite-plugin-css-injected-by-js@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-2.1.1.tgz#a79275241c61f1c8d55d228f5b2dded450a580e4"
|
||||
integrity sha512-gjIG6iFWde32oRr/bK9CsfN+jdbura2y4GlDzeOiEm6py38ud8dXzFl9zwmHjOjZdr8XEgQ9TovzVGNzp47esw==
|
||||
|
||||
vite@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.2.tgz#280762bfaf47bcea1d12698427331c0009ac7c1f"
|
||||
integrity sha512-pLrhatFFOWO9kS19bQ658CnRYzv0WLbsPih6R+iFeEEhDOuYgYCX2rztUViMz/uy/V8cLCJvLFeiOK7RJEzHcw==
|
||||
vite@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.3.tgz#7a68d9ef73eff7ee6dc0718ad3507adfc86944a7"
|
||||
integrity sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==
|
||||
dependencies:
|
||||
esbuild "^0.15.9"
|
||||
postcss "^8.4.18"
|
||||
@ -2031,16 +2162,16 @@ vue-tsc@^1.0.9:
|
||||
"@volar/vue-language-core" "1.0.9"
|
||||
"@volar/vue-typescript" "1.0.9"
|
||||
|
||||
vue@^3.2.41:
|
||||
version "3.2.41"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.41.tgz#ed452b8a0f7f2b962f055c8955139c28b1c06806"
|
||||
integrity sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ==
|
||||
vue@^3.2.45:
|
||||
version "3.2.45"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8"
|
||||
integrity sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.41"
|
||||
"@vue/compiler-sfc" "3.2.41"
|
||||
"@vue/runtime-dom" "3.2.41"
|
||||
"@vue/server-renderer" "3.2.41"
|
||||
"@vue/shared" "3.2.41"
|
||||
"@vue/compiler-dom" "3.2.45"
|
||||
"@vue/compiler-sfc" "3.2.45"
|
||||
"@vue/runtime-dom" "3.2.45"
|
||||
"@vue/server-renderer" "3.2.45"
|
||||
"@vue/shared" "3.2.45"
|
||||
|
||||
which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user