Merge remote-tracking branch 'tbnobody/OpenDTU/master'
15
.github/workflows/build.yml
vendored
@ -1,6 +1,14 @@
|
||||
name: OpenDTU Build
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
get_default_envs:
|
||||
@ -92,12 +100,15 @@ jobs:
|
||||
name: opendtu-${{ matrix.environment }}
|
||||
path: |
|
||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/partitions.bin
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: opendtu-release
|
||||
path: .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
path: |
|
||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/partitions.bin
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
|
||||
21
.github/workflows/cpplint.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: cpplint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install cpplint
|
||||
- name: Linting
|
||||
run: |
|
||||
cpplint --repository=. --recursive --filter=-runtime/references,-readability/braces,-whitespace,-legal,-build/include ./src ./include ./lib/Hoymiles
|
||||
65
README.md
@ -1,5 +1,8 @@
|
||||
# OpenDTU_VeDirect
|
||||
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
|
||||
|
||||
## Background
|
||||
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
|
||||
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
|
||||
@ -55,6 +58,7 @@ Sends text raw data as difined in VE.Direct spec.
|
||||
* Read live data from inverter
|
||||
* Show inverters internal event log
|
||||
* Show inverter information like firmware version, firmware build date, hardware revision and hardware version
|
||||
* Show current inverter limit (setting the limit is not yet implemented)
|
||||
* Uses ESP32 microcontroller and NRF24L01+
|
||||
* Multi-Inverter support
|
||||
* MQTT support (with TLS)
|
||||
@ -75,6 +79,39 @@ Sends text raw data as difined in VE.Direct spec.
|
||||
* Build with [Vue.js](https://vuejs.org)
|
||||
* Source is written in TypeScript
|
||||
|
||||
## Hardware you need
|
||||
|
||||
### ESP32 board
|
||||
For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it.
|
||||
|
||||
Sample Picture:
|
||||
|
||||

|
||||
|
||||
Also supported: Board with Ethernet-Connector and Power-over-Ethernet [Olimex ESP32-POE](https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware)
|
||||
|
||||
#### NRF24L01+ radio board
|
||||
The PLUS sign is IMPORTANT! There are different variants available, with antenna on the printed circuit board or external antenna.
|
||||
|
||||
Sample picture:
|
||||
|
||||

|
||||
|
||||
Buy your hardware from a trusted source, at best from a dealer/online shop in your country where you have support and the right to return non-functional hardware.
|
||||
When you want to buy from Amazon, AliExpress, eBay etc., take note that there is a lot of low-quality or fake hardware offered. Read customer comments and ratings carefully!
|
||||
|
||||
A heavily incomplete list of trusted hardware shops in germany is:
|
||||
|
||||
* [AZ-Delivery](https://www.az-delivery.de/)
|
||||
* [Makershop](https://www.makershop.de/)
|
||||
* [Berrybase](https://www.berrybase.de/)
|
||||
|
||||
This list is for your convenience only, the project is not related to any of these shops.
|
||||
|
||||
#### Power supply
|
||||
Use a power suppy with 5V and 1A. The USB cable connected to your PC/Notebook may be powerful enough or may be not.
|
||||
|
||||
|
||||
## Wiring up
|
||||
### Schematic
|
||||

|
||||
@ -98,11 +135,13 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or
|
||||
|
||||
## Flashing and starting up
|
||||
### with Visual Studio Code
|
||||
* Install [Visual Studio Code](https://code.visualstudio.com/download)
|
||||
* Install [Visual Studio Code](https://code.visualstudio.com/download) (from now named "vscode")
|
||||
* In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide)
|
||||
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur)
|
||||
* In Visual Studio Code, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" file)
|
||||
* Adjust the COM port in the file "platformio.ini". It occurs twice:
|
||||
* Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/)
|
||||
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory.
|
||||
* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" file)
|
||||
* There is a short [Video](https://youtu.be/9cA_esv3zeA) showing these steps.
|
||||
* Adjust the COM port in the file "platformio.ini" for your USB-serial-converter. It occurs twice:
|
||||
* upload_port
|
||||
* monitor_port
|
||||
* Select the arrow button in the status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically.
|
||||
@ -152,3 +191,21 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw
|
||||
|
||||
* Building the microcontroller firmware
|
||||
* Visual Studio Code with the PlatformIO Extension is required for building
|
||||
|
||||
## Troubleshooting
|
||||
* First: When there is no light on the solar panels, the inverter completely turns off and does not answer to OpenDTU! So if you assembled your OpenDTU in the evening, wait until tomorrow.
|
||||
* When there is no data received from the inverter(s) - try to reduce the distance between the openDTU and the inverter (e.g. move it to the window towards the roof)
|
||||
* Under Settings -> DTU Settings you can increase the transmit power "PA level". Default is "minimum".
|
||||
* The NRF24L01+ needs relatively much current. With bad power supply (and especially bad cables!) a 10uF capacitor soldered directly to the NRF24L01+ board connector brings more stability (pin 1+2 are the power supply). Note the polarity of the capacitor....
|
||||
* You can try to use an USB power supply with 1A or more instead of connecting the ESP32 to the computer.
|
||||
* Try a different USB cable. Once again, a stable power source is important. Some USB cables are made of much plastic and very little copper inside.
|
||||
* Double-Check that you have a radio module NRF24L01+ with a plus sign at the end. NRF24L01 module without the plus are not compatible with this project.
|
||||
* There is no possibility of auto-discovering the inverters. Double-Check you have entered the serial numbers of the inverters correctly.
|
||||
* OpenDTU needs access to a working NTP server to get the current date & time.
|
||||
* If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information.
|
||||
* Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/)
|
||||
|
||||
## Related Projects
|
||||
- [Ahoy](https://github.com/grindylow/ahoy)
|
||||
- [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles)
|
||||
- [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect)
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
BIN
docs/nodemcu-esp32.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
docs/nrf24l01plus.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 57 KiB |
BIN
docs/screenshots/14_ConfigManagement.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
@ -50,7 +50,7 @@ public:
|
||||
IPAddress gatewayIP();
|
||||
IPAddress dnsIP(uint8_t dns_no = 0);
|
||||
String macAddress();
|
||||
const char* getHostname();
|
||||
static String getHostname();
|
||||
bool isConnected();
|
||||
network_mode NetworkMode();
|
||||
|
||||
@ -62,12 +62,13 @@ private:
|
||||
void setStaticIp();
|
||||
void setupMode();
|
||||
void NetworkEvent(WiFiEvent_t event);
|
||||
static uint32_t getChipId();
|
||||
bool adminEnabled = true;
|
||||
bool forceDisconnection = false;
|
||||
int adminTimeoutCounter = 0;
|
||||
int connectTimeoutTimer = 0;
|
||||
int connectRedoTimer = 0;
|
||||
unsigned long lastTimerCall = 0;
|
||||
uint32_t lastTimerCall = 0;
|
||||
const byte DNS_PORT = 53;
|
||||
IPAddress apIp;
|
||||
IPAddress apNetmask;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "WebApi_config.h"
|
||||
#include "WebApi_devinfo.h"
|
||||
#include "WebApi_dtu.h"
|
||||
#include "WebApi_eventlog.h"
|
||||
#include "WebApi_firmware.h"
|
||||
#include "WebApi_inverter.h"
|
||||
#include "WebApi_limit.h"
|
||||
#include "WebApi_mqtt.h"
|
||||
#include "WebApi_network.h"
|
||||
#include "WebApi_ntp.h"
|
||||
@ -26,11 +28,13 @@ private:
|
||||
AsyncWebServer _server;
|
||||
AsyncEventSource _events;
|
||||
|
||||
WebApiConfigClass _webApiConfig;
|
||||
WebApiDevInfoClass _webApiDevInfo;
|
||||
WebApiDtuClass _webApiDtu;
|
||||
WebApiEventlogClass _webApiEventlog;
|
||||
WebApiFirmwareClass _webApiFirmware;
|
||||
WebApiInverterClass _webApiInverter;
|
||||
WebApiLimitClass _webApiLimit;
|
||||
WebApiMqttClass _webApiMqtt;
|
||||
WebApiNetworkClass _webApiNetwork;
|
||||
WebApiNtpClass _webApiNtp;
|
||||
|
||||
18
include/WebApi_config.h
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiConfigClass {
|
||||
public:
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onConfigGet(AsyncWebServerRequest* request);
|
||||
void onConfigDelete(AsyncWebServerRequest* request);
|
||||
void onConfigUploadFinish(AsyncWebServerRequest* request);
|
||||
void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
15
include/WebApi_limit.h
Normal file
@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiLimitClass {
|
||||
public:
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onLimitStatus(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -14,7 +14,7 @@ private:
|
||||
void onMqttStatus(AsyncWebServerRequest* request);
|
||||
void onMqttAdminGet(AsyncWebServerRequest* request);
|
||||
void onMqttAdminPost(AsyncWebServerRequest* request);
|
||||
String getRootCaCertInfo(char* cert);
|
||||
String getRootCaCertInfo(const char* cert);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -22,6 +22,6 @@ private:
|
||||
|
||||
uint32_t _lastWsPublish = 0;
|
||||
uint32_t _lastInvUpdateCheck = 0;
|
||||
unsigned long _lastWsCleanup = 0;
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _newestInverterTimestamp = 0;
|
||||
};
|
||||
@ -31,6 +31,12 @@ void HoymilesClass::loop()
|
||||
// Fetch event log
|
||||
iv->sendAlarmLogRequest(_radio.get());
|
||||
|
||||
// Fetch limit
|
||||
if ((iv->SystemConfigPara()->getLastUpdate() == 0) || (millis() - iv->SystemConfigPara()->getLastUpdate() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL)) {
|
||||
Serial.println("Request SystemConfigPara");
|
||||
iv->sendSystemConfigParaRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Fetch dev info (but first fetch stats)
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSample() == 0)) {
|
||||
Serial.println(F("Request device info"));
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#define HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL (10 * 60 * 1000) // 10 minutes
|
||||
|
||||
class HoymilesClass {
|
||||
public:
|
||||
void init();
|
||||
@ -28,7 +30,7 @@ private:
|
||||
std::vector<std::shared_ptr<InverterAbstract>> _inverters;
|
||||
std::unique_ptr<HoymilesRadio> _radio;
|
||||
|
||||
uint32_t _pollInterval;
|
||||
uint32_t _pollInterval = 0;
|
||||
uint32_t _lastPoll = 0;
|
||||
};
|
||||
|
||||
|
||||
@ -9,8 +9,6 @@
|
||||
#include <nRF24L01.h>
|
||||
#include <queue>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// number of fragments hold in buffer
|
||||
#define FRAGMENT_BUFFER_SIZE 30
|
||||
|
||||
@ -57,7 +55,7 @@ public:
|
||||
template <typename T>
|
||||
T* enqueCommand()
|
||||
{
|
||||
_commandQueue.push(make_shared<T>());
|
||||
_commandQueue.push(std::make_shared<T>());
|
||||
return static_cast<T*>(_commandQueue.back().get());
|
||||
}
|
||||
|
||||
@ -75,12 +73,12 @@ private:
|
||||
std::unique_ptr<SPIClass> _hspi;
|
||||
std::unique_ptr<RF24> _radio;
|
||||
uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 };
|
||||
uint8_t _rxChIdx;
|
||||
uint8_t _rxChIdx = 0;
|
||||
|
||||
uint8_t _txChLst[5] = { 3, 23, 40, 61, 75 };
|
||||
uint8_t _txChIdx;
|
||||
uint8_t _txChIdx = 0;
|
||||
|
||||
volatile bool _packetReceived;
|
||||
volatile bool _packetReceived = false;
|
||||
|
||||
CircularBuffer<fragment_t, FRAGMENT_BUFFER_SIZE> _rxBuffer;
|
||||
TimeoutHelper _rxTimeout;
|
||||
@ -89,5 +87,5 @@ private:
|
||||
|
||||
bool _busyFlag = false;
|
||||
|
||||
queue<shared_ptr<CommandAbstract>> _commandQueue;
|
||||
std::queue<std::shared_ptr<CommandAbstract>> _commandQueue;
|
||||
};
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class AlarmDataCommand : public MultiDataCommand {
|
||||
public:
|
||||
AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
explicit AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -10,7 +10,7 @@ class InverterAbstract;
|
||||
|
||||
class CommandAbstract {
|
||||
public:
|
||||
CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
explicit CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
virtual ~CommandAbstract() {};
|
||||
|
||||
template <typename T>
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
|
||||
class DevControlCommand : public CommandAbstract {
|
||||
public:
|
||||
DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
explicit DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
};
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class DevInfoAllCommand : public MultiDataCommand {
|
||||
public:
|
||||
DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
explicit DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class DevInfoSampleCommand : public MultiDataCommand {
|
||||
public:
|
||||
DevInfoSampleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
explicit DevInfoSampleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
class MultiDataCommand : public CommandAbstract {
|
||||
public:
|
||||
MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0);
|
||||
explicit MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0);
|
||||
|
||||
void setTime(time_t time);
|
||||
time_t getTime();
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
|
||||
class ParaSetCommand : public CommandAbstract {
|
||||
public:
|
||||
ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
explicit ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
};
|
||||
13
lib/Hoymiles/src/commands/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Class hierarchy
|
||||
|
||||
* CommandAbstract
|
||||
* DevControlCommand
|
||||
* MultiDataCommand
|
||||
* AlarmDataCommand
|
||||
* DevInfoAllCommand
|
||||
* DevInfoSampleCommand
|
||||
* RealTimeRunDataCommand
|
||||
* SystemConfigParaCommand
|
||||
* ParaSetCommand
|
||||
* SingleDataCommand
|
||||
* RequestFrameCommand
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class RealTimeRunDataCommand : public MultiDataCommand {
|
||||
public:
|
||||
RealTimeRunDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
explicit RealTimeRunDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class RequestFrameCommand : public SingleDataCommand {
|
||||
public:
|
||||
RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0);
|
||||
explicit RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0);
|
||||
|
||||
void setFrameNo(uint8_t frame_no);
|
||||
uint8_t getFrameNo();
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
|
||||
class SingleDataCommand : public CommandAbstract {
|
||||
public:
|
||||
SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
explicit SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0);
|
||||
};
|
||||
28
lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp
Normal file
@ -0,0 +1,28 @@
|
||||
#include "SystemConfigParaCommand.h"
|
||||
#include "inverters/InverterAbstract.h"
|
||||
|
||||
SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64_t router_address, time_t time)
|
||||
: MultiDataCommand(target_address, router_address)
|
||||
{
|
||||
setTime(time);
|
||||
setDataType(0x05);
|
||||
setTimeout(200);
|
||||
}
|
||||
|
||||
bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id)
|
||||
{
|
||||
// Check CRC of whole payload
|
||||
if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move all fragments into target buffer
|
||||
uint8_t offs = 0;
|
||||
inverter->SystemConfigPara()->clearBuffer();
|
||||
for (uint8_t i = 0; i < max_fragment_id; i++) {
|
||||
inverter->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len);
|
||||
offs += (fragment[i].len);
|
||||
}
|
||||
inverter->SystemConfigPara()->setLastUpdate(millis());
|
||||
return true;
|
||||
}
|
||||
10
lib/Hoymiles/src/commands/SystemConfigParaCommand.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "MultiDataCommand.h"
|
||||
|
||||
class SystemConfigParaCommand : public MultiDataCommand {
|
||||
public:
|
||||
explicit SystemConfigParaCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
|
||||
|
||||
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
#include "crc.h"
|
||||
|
||||
uint8_t crc8(uint8_t buf[], uint8_t len)
|
||||
uint8_t crc8(const uint8_t buf[], uint8_t len)
|
||||
{
|
||||
uint8_t crc = CRC8_INIT;
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
@ -12,7 +12,7 @@ uint8_t crc8(uint8_t buf[], uint8_t len)
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start)
|
||||
uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start)
|
||||
{
|
||||
uint16_t crc = start;
|
||||
uint8_t shift = 0;
|
||||
@ -29,7 +29,7 @@ uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start)
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit, uint16_t crcIn)
|
||||
uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit, uint16_t crcIn)
|
||||
{
|
||||
uint16_t crc = crcIn;
|
||||
uint8_t idx, val = buf[(startBit >> 3)];
|
||||
|
||||
@ -8,6 +8,6 @@
|
||||
#define CRC16_MODBUS_POLYNOM 0xA001
|
||||
#define CRC16_NRF24_POLYNOM 0x1021
|
||||
|
||||
uint8_t crc8(uint8_t buf[], uint8_t len);
|
||||
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff);
|
||||
uint16_t crc16nrf24(uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff);
|
||||
uint8_t crc8(const uint8_t buf[], uint8_t len);
|
||||
uint16_t crc16(const uint8_t buf[], uint8_t len, uint16_t start = 0xffff);
|
||||
uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class HM_1CH : public HM_Abstract {
|
||||
public:
|
||||
HM_1CH(uint64_t serial);
|
||||
explicit HM_1CH(uint64_t serial);
|
||||
static bool isValidSerial(uint64_t serial);
|
||||
String typeName();
|
||||
const byteAssign_t* getByteAssignment();
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class HM_2CH : public HM_Abstract {
|
||||
public:
|
||||
HM_2CH(uint64_t serial);
|
||||
explicit HM_2CH(uint64_t serial);
|
||||
static bool isValidSerial(uint64_t serial);
|
||||
String typeName();
|
||||
const byteAssign_t* getByteAssignment();
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
class HM_4CH : public HM_Abstract {
|
||||
public:
|
||||
HM_4CH(uint64_t serial);
|
||||
explicit HM_4CH(uint64_t serial);
|
||||
static bool isValidSerial(uint64_t serial);
|
||||
String typeName();
|
||||
const byteAssign_t* getByteAssignment();
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include "commands/DevInfoAllCommand.h"
|
||||
#include "commands/DevInfoSampleCommand.h"
|
||||
#include "commands/RealTimeRunDataCommand.h"
|
||||
#include "commands/SystemConfigParaCommand.h"
|
||||
|
||||
HM_Abstract::HM_Abstract(uint64_t serial)
|
||||
: InverterAbstract(serial) {};
|
||||
@ -68,5 +69,22 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
|
||||
cmdSample->setTime(now);
|
||||
cmdSample->setTargetAddress(serial());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
|
||||
{
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
|
||||
SystemConfigParaCommand* cmd = radio->enqueCommand<SystemConfigParaCommand>();
|
||||
cmd->setTime(now);
|
||||
cmd->setTargetAddress(serial());
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -4,10 +4,11 @@
|
||||
|
||||
class HM_Abstract : public InverterAbstract {
|
||||
public:
|
||||
HM_Abstract(uint64_t serial);
|
||||
explicit HM_Abstract(uint64_t serial);
|
||||
bool sendStatsRequest(HoymilesRadio* radio);
|
||||
bool sendAlarmLogRequest(HoymilesRadio* radio);
|
||||
bool sendDevInfoRequest(HoymilesRadio* radio);
|
||||
bool sendSystemConfigParaRequest(HoymilesRadio* radio);
|
||||
|
||||
private:
|
||||
uint8_t _lastAlarmLogCnt = 0;
|
||||
|
||||
@ -8,6 +8,7 @@ InverterAbstract::InverterAbstract(uint64_t serial)
|
||||
_alarmLogParser.reset(new AlarmLogParser());
|
||||
_devInfoParser.reset(new DevInfoParser());
|
||||
_statisticsParser.reset(new StatisticsParser());
|
||||
_systemConfigParaParser.reset(new SystemConfigParaParser());
|
||||
}
|
||||
|
||||
void InverterAbstract::init()
|
||||
@ -54,6 +55,11 @@ StatisticsParser* InverterAbstract::Statistics()
|
||||
return _statisticsParser.get();
|
||||
}
|
||||
|
||||
SystemConfigParaParser* InverterAbstract::SystemConfigPara()
|
||||
{
|
||||
return _systemConfigParaParser.get();
|
||||
}
|
||||
|
||||
void InverterAbstract::clearRxFragmentBuffer()
|
||||
{
|
||||
memset(_rxFragmentBuffer, 0, MAX_RF_FRAGMENT_COUNT * sizeof(fragment_t));
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "../parser/AlarmLogParser.h"
|
||||
#include "../parser/StatisticsParser.h"
|
||||
#include "../parser/DevInfoParser.h"
|
||||
#include "../parser/StatisticsParser.h"
|
||||
#include "../parser/SystemConfigParaParser.h"
|
||||
#include "HoymilesRadio.h"
|
||||
#include "types.h"
|
||||
#include <Arduino.h>
|
||||
@ -24,7 +25,7 @@ class CommandAbstract;
|
||||
|
||||
class InverterAbstract {
|
||||
public:
|
||||
InverterAbstract(uint64_t serial);
|
||||
explicit InverterAbstract(uint64_t serial);
|
||||
void init();
|
||||
uint64_t serial();
|
||||
void setName(const char* name);
|
||||
@ -40,14 +41,16 @@ public:
|
||||
virtual bool sendStatsRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendAlarmLogRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendDevInfoRequest(HoymilesRadio* radio) = 0;
|
||||
virtual bool sendSystemConfigParaRequest(HoymilesRadio* radio) = 0;
|
||||
|
||||
AlarmLogParser* EventLog();
|
||||
DevInfoParser* DevInfo();
|
||||
StatisticsParser* Statistics();
|
||||
SystemConfigParaParser* SystemConfigPara();
|
||||
|
||||
private:
|
||||
serial_u _serial;
|
||||
char _name[MAX_NAME_LENGTH];
|
||||
char _name[MAX_NAME_LENGTH] = "";
|
||||
fragment_t _rxFragmentBuffer[MAX_RF_FRAGMENT_COUNT];
|
||||
uint8_t _rxFragmentMaxPacketId = 0;
|
||||
uint8_t _rxFragmentLastPacketId = 0;
|
||||
@ -56,4 +59,5 @@ private:
|
||||
std::unique_ptr<AlarmLogParser> _alarmLogParser;
|
||||
std::unique_ptr<DevInfoParser> _devInfoParser;
|
||||
std::unique_ptr<StatisticsParser> _statisticsParser;
|
||||
std::unique_ptr<SystemConfigParaParser> _systemConfigParaParser;
|
||||
};
|
||||
@ -272,5 +272,5 @@ int AlarmLogParser::getTimezoneOffset()
|
||||
ptm->tm_isdst = -1;
|
||||
gmt = mktime(ptm);
|
||||
|
||||
return (int)difftime(rawtime, gmt);
|
||||
return static_cast<int>(difftime(rawtime, gmt));
|
||||
}
|
||||
@ -98,7 +98,7 @@ uint16_t DevInfoParser::getHwVersion()
|
||||
/* struct tm to seconds since Unix epoch */
|
||||
time_t DevInfoParser::timegm(struct tm* t)
|
||||
{
|
||||
register long year;
|
||||
register uint32_t year;
|
||||
register time_t result;
|
||||
#define MONTHSPERYEAR 12 /* months per calendar year */
|
||||
static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include "Parser.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
@ -30,9 +31,9 @@ private:
|
||||
uint32_t _lastUpdateAll = 0;
|
||||
uint32_t _lastUpdateSample = 0;
|
||||
|
||||
uint8_t _payloadDevInfoAll[DEV_INFO_SIZE];
|
||||
uint8_t _devInfoAllLength;
|
||||
uint8_t _payloadDevInfoAll[DEV_INFO_SIZE] = {};
|
||||
uint8_t _devInfoAllLength = 0;
|
||||
|
||||
uint8_t _payloadDevInfoSample[DEV_INFO_SIZE];
|
||||
uint8_t _devInfoSampleLength;
|
||||
uint8_t _payloadDevInfoSample[DEV_INFO_SIZE] = {};
|
||||
uint8_t _devInfoSampleLength = 0;
|
||||
};
|
||||
@ -56,7 +56,7 @@ float StatisticsParser::getChannelFieldValue(uint8_t channel, uint8_t fieldId)
|
||||
val |= _payloadStatistic[ptr];
|
||||
} while (++ptr != end);
|
||||
|
||||
return (float)(val) / (float)(div);
|
||||
return static_cast<float>(val) / static_cast<float>(div);
|
||||
} else {
|
||||
// Value has to be calculated
|
||||
return calcFunctions[b[pos].start].func(this, b[pos].num);
|
||||
|
||||
@ -114,8 +114,8 @@ public:
|
||||
void setChannelMaxPower(uint8_t channel, uint16_t power);
|
||||
|
||||
private:
|
||||
uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE];
|
||||
uint8_t _statisticLength;
|
||||
uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {};
|
||||
uint8_t _statisticLength = 0;
|
||||
uint16_t _chanMaxPower[CH4];
|
||||
|
||||
const byteAssign_t* _byteAssignment;
|
||||
|
||||
23
lib/Hoymiles/src/parser/SystemConfigParaParser.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
#include "SystemConfigParaParser.h"
|
||||
#include <cstring>
|
||||
|
||||
void SystemConfigParaParser::clearBuffer()
|
||||
{
|
||||
memset(_payload, 0, SYSTEM_CONFIG_PARA_SIZE);
|
||||
_payloadLength = 0;
|
||||
}
|
||||
|
||||
void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len)
|
||||
{
|
||||
if (offset + len > (SYSTEM_CONFIG_PARA_SIZE)) {
|
||||
Serial.printf("FATAL: (%s, %d) stats packet too large for buffer\n", __FILE__, __LINE__);
|
||||
return;
|
||||
}
|
||||
memcpy(&_payload[offset], payload, len);
|
||||
_payloadLength += len;
|
||||
}
|
||||
|
||||
float SystemConfigParaParser::getLimitPercent()
|
||||
{
|
||||
return ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10;
|
||||
}
|
||||
17
lib/Hoymiles/src/parser/SystemConfigParaParser.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "Parser.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
#define SYSTEM_CONFIG_PARA_SIZE 16
|
||||
|
||||
class SystemConfigParaParser : public Parser {
|
||||
public:
|
||||
void clearBuffer();
|
||||
void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len);
|
||||
|
||||
float getLimitPercent();
|
||||
|
||||
private:
|
||||
uint8_t _payload[SYSTEM_CONFIG_PARA_SIZE];
|
||||
uint8_t _payloadLength;
|
||||
};
|
||||
159
platformio.ini
@ -1,68 +1,91 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = generic
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
platform = espressif32@>=5
|
||||
build_flags =
|
||||
-D=${PIOENV}
|
||||
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
|
||||
lib_deps =
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||
bblanchon/ArduinoJson @ ^6.19.4
|
||||
https://github.com/bertmelis/espMqttClient.git
|
||||
nrf24/RF24 @ ^1.4.5
|
||||
extra_scripts = pre:auto_firmware_version.py
|
||||
board_build.partitions = partitions_custom.csv
|
||||
board_build.filesystem = littlefs
|
||||
monitor_filters =
|
||||
time
|
||||
colorize
|
||||
log2file
|
||||
esp32_exception_decoder
|
||||
monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
|
||||
[env:generic]
|
||||
board = esp32dev
|
||||
monitor_port = COM4
|
||||
upload_port = COM4
|
||||
|
||||
[env:olimex_esp32_poe]
|
||||
board = esp32-poe
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DOPENDTU_ETHERNET
|
||||
monitor_port = COM3
|
||||
upload_port = COM3
|
||||
|
||||
[env:d1 mini esp32]
|
||||
board = wemos_d1_mini32
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DVICTRON_PIN_TX=21
|
||||
-DVICTRON_PIN_RX=22
|
||||
monitor_port = /dev/cu.usbserial-01E68DD0
|
||||
upload_port = /dev/cu.usbserial-01E68DD0
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = generic
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
platform = espressif32@>=5
|
||||
|
||||
build_flags =
|
||||
-D=${PIOENV}
|
||||
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
|
||||
|
||||
lib_deps =
|
||||
https://github.com/yubox-node-org/ESPAsyncWebServer
|
||||
bblanchon/ArduinoJson @ ^6.19.4
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.2.3
|
||||
nrf24/RF24 @ ^1.4.5
|
||||
|
||||
extra_scripts =
|
||||
pre:auto_firmware_version.py
|
||||
|
||||
board_build.partitions = partitions_custom.csv
|
||||
board_build.filesystem = littlefs
|
||||
monitor_filters = time, colorize, log2file, esp32_exception_decoder
|
||||
monitor_speed = 115200
|
||||
upload_protocol = esptool
|
||||
|
||||
|
||||
[env:generic]
|
||||
board = esp32dev
|
||||
monitor_port = COM4
|
||||
upload_port = COM4
|
||||
|
||||
|
||||
[env:olimex_esp32_poe]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
|
||||
|
||||
board = esp32-poe
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
monitor_port = COM3
|
||||
upload_port = COM3
|
||||
|
||||
|
||||
[env:olimex_esp32_evb]
|
||||
; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware
|
||||
|
||||
board = esp32-evb
|
||||
build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=15
|
||||
-DHOYMILES_PIN_MOSI=2
|
||||
-DHOYMILES_PIN_SCLK=14
|
||||
-DHOYMILES_PIN_IRQ=13
|
||||
-DHOYMILES_PIN_CE=16
|
||||
-DHOYMILES_PIN_CS=17
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
monitor_port = /dev/tty.usbserial-1450
|
||||
upload_port = /dev/tty.usbserial-1450
|
||||
|
||||
|
||||
[env:d1 mini esp32]
|
||||
board = wemos_d1_mini32
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
-DHOYMILES_PIN_MISO=19
|
||||
-DHOYMILES_PIN_MOSI=23
|
||||
-DHOYMILES_PIN_SCLK=18
|
||||
-DHOYMILES_PIN_IRQ=16
|
||||
-DHOYMILES_PIN_CE=17
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DVICTRON_PIN_TX=21
|
||||
-DVICTRON_PIN_RX=22
|
||||
monitor_port = /dev/cu.usbserial-01E68DD0
|
||||
upload_port = /dev/cu.usbserial-01E68DD0
|
||||
|
||||
@ -70,7 +70,7 @@ bool ConfigurationClass::write()
|
||||
return false;
|
||||
}
|
||||
config.Cfg_SaveCount++;
|
||||
uint8_t* bytes = (uint8_t*)&config;
|
||||
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
|
||||
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
|
||||
f.write(bytes[i]);
|
||||
}
|
||||
@ -84,7 +84,7 @@ bool ConfigurationClass::read()
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
uint8_t* bytes = (uint8_t*)&config;
|
||||
uint8_t* bytes = reinterpret_cast<uint8_t*>(&config);
|
||||
for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
|
||||
bytes[i] = f.read();
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ void MqttHassPublishingClass::publishConfig()
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
// Loop all inverters
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
@ -74,7 +74,7 @@ void MqttHassPublishingClass::publishField(std::shared_ptr<InverterAbstract> inv
|
||||
}
|
||||
|
||||
char serial[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(serial, "%0lx%08lx",
|
||||
snprintf(serial, sizeof(serial), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
|
||||
@ -19,18 +19,21 @@ void MqttPublishingClass::loop()
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) {
|
||||
MqttSettings.publish("dtu/uptime", String(millis() / 1000));
|
||||
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
|
||||
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
|
||||
MqttSettings.publish("dtu/rssi", String(WiFi.RSSI()));
|
||||
}
|
||||
|
||||
// Loop all inverters
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
String subtopic = String(buffer);
|
||||
@ -58,6 +61,11 @@ void MqttPublishingClass::loop()
|
||||
MqttSettings.publish(subtopic + "/device/hwversion", String(inv->DevInfo()->getHwVersion()));
|
||||
}
|
||||
|
||||
if (inv->SystemConfigPara()->getLastUpdate() > 0) {
|
||||
// Limit
|
||||
MqttSettings.publish(subtopic + "/settings/limit", String(inv->SystemConfigPara()->getLimitPercent()));
|
||||
}
|
||||
|
||||
uint32_t lastUpdate = inv->Statistics()->getLastUpdate();
|
||||
if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) {
|
||||
_lastPublishStats[i] = lastUpdate;
|
||||
@ -94,7 +102,7 @@ String MqttPublishingClass::getTopic(std::shared_ptr<InverterAbstract> inv, uint
|
||||
}
|
||||
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
String invSerial = String(buffer);
|
||||
|
||||
@ -30,7 +30,7 @@ void MqttSettingsClass::NetworkEvent(network_event event)
|
||||
void MqttSettingsClass::onMqttConnect(bool sessionPresent)
|
||||
{
|
||||
Serial.println(F("Connected to MQTT."));
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online);
|
||||
}
|
||||
|
||||
@ -68,9 +68,9 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
|
||||
void MqttSettingsClass::performConnect()
|
||||
{
|
||||
if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) {
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
Serial.println(F("Connecting to MQTT..."));
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
willTopic = getPrefix() + config.Mqtt_LwtTopic;
|
||||
clientId = NetworkSettings.getApName();
|
||||
if (config.Mqtt_Tls) {
|
||||
@ -95,7 +95,7 @@ void MqttSettingsClass::performConnect()
|
||||
|
||||
void MqttSettingsClass::performDisconnect()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Offline);
|
||||
mqttClient->disconnect();
|
||||
}
|
||||
@ -136,7 +136,7 @@ void MqttSettingsClass::publishHass(String subtopic, String payload)
|
||||
|
||||
void MqttSettingsClass::init()
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
NetworkSettings.onEvent(std::bind(&MqttSettingsClass::NetworkEvent, this, _1));
|
||||
|
||||
createMqttClientObject();
|
||||
@ -146,7 +146,7 @@ void MqttSettingsClass::createMqttClientObject()
|
||||
{
|
||||
if (mqttClient != nullptr)
|
||||
delete mqttClient;
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
if (config.Mqtt_Tls) {
|
||||
mqttClient = static_cast<MqttClient*>(new espMqttClientSecure);
|
||||
} else {
|
||||
|
||||
@ -19,7 +19,7 @@ NetworkSettingsClass::NetworkSettingsClass()
|
||||
|
||||
void NetworkSettingsClass::init()
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1));
|
||||
setupMode();
|
||||
@ -142,11 +142,7 @@ void NetworkSettingsClass::enableAdminMode()
|
||||
|
||||
String NetworkSettingsClass::getApName()
|
||||
{
|
||||
uint32_t chipId = 0;
|
||||
for (int i = 0; i < 17; i += 8) {
|
||||
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
return String(ACCESS_POINT_NAME + String(chipId));
|
||||
return String(ACCESS_POINT_NAME + String(getChipId()));
|
||||
}
|
||||
|
||||
void NetworkSettingsClass::loop()
|
||||
@ -241,32 +237,37 @@ void NetworkSettingsClass::applyConfig()
|
||||
void NetworkSettingsClass::setHostname()
|
||||
{
|
||||
Serial.print(F("Setting Hostname... "));
|
||||
if (strcmp(Configuration.get().WiFi_Hostname, "")) {
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
if (WiFi.hostname(Configuration.get().WiFi_Hostname)) {
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.println(F("failed"));
|
||||
}
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
if (WiFi.hostname(getHostname())) {
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.println(F("failed"));
|
||||
}
|
||||
#ifdef OPENDTU_ETHERNET
|
||||
else if (_networkMode == network_mode::Ethernet) {
|
||||
if (ETH.setHostname(Configuration.get().WiFi_Hostname)) {
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.println(F("failed"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
Serial.println(F("failed (Hostname empty)"));
|
||||
|
||||
// Evil bad hack to get the hostname set up correctly
|
||||
WiFi.mode(WIFI_MODE_APSTA);
|
||||
WiFi.mode(WIFI_MODE_STA);
|
||||
setupMode();
|
||||
}
|
||||
#ifdef OPENDTU_ETHERNET
|
||||
else if (_networkMode == network_mode::Ethernet) {
|
||||
if (ETH.setHostname(getHostname().c_str())) {
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.println(F("failed"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void NetworkSettingsClass::setStaticIp()
|
||||
{
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
if (!Configuration.get().WiFi_Dhcp) {
|
||||
if (Configuration.get().WiFi_Dhcp) {
|
||||
Serial.print(F("Configuring WiFi STA DHCP IP... "));
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.print(F("Configuring WiFi STA static IP... "));
|
||||
WiFi.config(
|
||||
IPAddress(Configuration.get().WiFi_Ip),
|
||||
@ -280,7 +281,9 @@ void NetworkSettingsClass::setStaticIp()
|
||||
#ifdef OPENDTU_ETHERNET
|
||||
else if (_networkMode == network_mode::Ethernet) {
|
||||
if (Configuration.get().WiFi_Dhcp) {
|
||||
Serial.print(F("Configuring Ethernet DHCP IP... "));
|
||||
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
Serial.println(F("done"));
|
||||
} else {
|
||||
Serial.print(F("Configuring Ethernet static IP... "));
|
||||
ETH.config(
|
||||
@ -375,14 +378,43 @@ String NetworkSettingsClass::macAddress()
|
||||
}
|
||||
}
|
||||
|
||||
const char* NetworkSettingsClass::getHostname()
|
||||
String NetworkSettingsClass::getHostname()
|
||||
{
|
||||
#ifdef OPENDTU_ETHERNET
|
||||
if (_networkMode == network_mode::Ethernet) {
|
||||
return ETH.getHostname();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
char preparedHostname[WIFI_MAX_HOSTNAME_STRLEN + 1];
|
||||
char resultHostname[WIFI_MAX_HOSTNAME_STRLEN + 1];
|
||||
uint8_t pos = 0;
|
||||
|
||||
uint32_t chipId = getChipId();
|
||||
snprintf(preparedHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, config.WiFi_Hostname, chipId);
|
||||
|
||||
const char* pC = preparedHostname;
|
||||
while (*pC && pos < WIFI_MAX_HOSTNAME_STRLEN) { // while !null and not over length
|
||||
if (isalnum(*pC)) { // if the current char is alpha-numeric append it to the hostname
|
||||
resultHostname[pos] = *pC;
|
||||
pos++;
|
||||
} else if (*pC == ' ' || *pC == '_' || *pC == '-' || *pC == '+' || *pC == '!' || *pC == '?' || *pC == '*') {
|
||||
resultHostname[pos] = '-';
|
||||
pos++;
|
||||
}
|
||||
// else do nothing - no leading hyphens and do not include hyphens for all other characters.
|
||||
pC++;
|
||||
}
|
||||
#endif
|
||||
return WiFi.getHostname();
|
||||
|
||||
resultHostname[pos] = '\0'; // terminate string
|
||||
|
||||
// last character must not be hyphen
|
||||
while (pos > 0 && resultHostname[pos - 1] == '-') {
|
||||
resultHostname[pos - 1] = '\0';
|
||||
pos--;
|
||||
}
|
||||
|
||||
// Fallback if no other rule applied
|
||||
if (strlen(resultHostname) == 0) {
|
||||
snprintf(resultHostname, WIFI_MAX_HOSTNAME_STRLEN + 1, APP_HOSTNAME, chipId);
|
||||
}
|
||||
|
||||
return resultHostname;
|
||||
}
|
||||
|
||||
bool NetworkSettingsClass::isConnected()
|
||||
@ -399,4 +431,13 @@ network_mode NetworkSettingsClass::NetworkMode()
|
||||
return _networkMode;
|
||||
}
|
||||
|
||||
uint32_t NetworkSettingsClass::getChipId()
|
||||
{
|
||||
uint32_t chipId = 0;
|
||||
for (int i = 0; i < 17; i += 8) {
|
||||
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
return chipId;
|
||||
}
|
||||
|
||||
NetworkSettingsClass NetworkSettings;
|
||||
@ -15,15 +15,15 @@ WebApiClass::WebApiClass()
|
||||
|
||||
void WebApiClass::init()
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
|
||||
_server.addHandler(&_events);
|
||||
|
||||
_webApiConfig.init(&_server);
|
||||
_webApiDevInfo.init(&_server);
|
||||
_webApiDtu.init(&_server);
|
||||
_webApiEventlog.init(&_server);
|
||||
_webApiFirmware.init(&_server);
|
||||
_webApiInverter.init(&_server);
|
||||
_webApiLimit.init(&_server);
|
||||
_webApiMqtt.init(&_server);
|
||||
_webApiNetwork.init(&_server);
|
||||
_webApiNtp.init(&_server);
|
||||
@ -38,11 +38,13 @@ void WebApiClass::init()
|
||||
|
||||
void WebApiClass::loop()
|
||||
{
|
||||
_webApiConfig.loop();
|
||||
_webApiDevInfo.loop();
|
||||
_webApiDtu.loop();
|
||||
_webApiEventlog.loop();
|
||||
_webApiFirmware.loop();
|
||||
_webApiInverter.loop();
|
||||
_webApiLimit.loop();
|
||||
_webApiMqtt.loop();
|
||||
_webApiNetwork.loop();
|
||||
_webApiNtp.loop();
|
||||
|
||||
125
src/WebApi_config.cpp
Normal file
@ -0,0 +1,125 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_config.h"
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include <LittleFS.h>
|
||||
|
||||
void WebApiConfigClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1));
|
||||
_server->on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1));
|
||||
_server->on("/api/config/upload", HTTP_POST,
|
||||
std::bind(&WebApiConfigClass::onConfigUploadFinish, this, _1),
|
||||
std::bind(&WebApiConfigClass::onConfigUpload, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
void WebApiConfigClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
request->send(LittleFS, CONFIG_FILENAME, String(), true);
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("delete"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("delete")].as<bool>() == false) {
|
||||
retMsg[F("message")] = F("Not deleted anything!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Configuration resettet. Rebooting now...");
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
LittleFS.remove(CONFIG_FILENAME);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
|
||||
{
|
||||
// the request handler is triggered after the upload has finished...
|
||||
// create the response, add header, and send response
|
||||
|
||||
AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", "OK");
|
||||
response->addHeader("Connection", "close");
|
||||
response->addHeader("Access-Control-Allow-Origin", "*");
|
||||
request->send(response);
|
||||
yield();
|
||||
delay(1000);
|
||||
yield();
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)
|
||||
{
|
||||
if (!index) {
|
||||
// open the file on first call and store the file handle in the request object
|
||||
request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w");
|
||||
}
|
||||
|
||||
if (len) {
|
||||
// stream the incoming chunk to the opened file
|
||||
request->_tempFile.write(data, len);
|
||||
}
|
||||
|
||||
if (final) {
|
||||
// close the file handle as the upload is now done
|
||||
request->_tempFile.close();
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
void WebApiDevInfoClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -31,7 +31,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
|
||||
|
||||
// Inverter Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
void WebApiDtuClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -26,11 +26,11 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
// DTU Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF)));
|
||||
root[F("dtu_serial")] = buffer;
|
||||
@ -102,9 +102,9 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
char* t;
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
config.Dtu_Serial = strtoll(root[F("dtu_serial")].as<String>().c_str(), &t, 16);
|
||||
config.Dtu_Serial = strtoll(root[F("dtu_serial")].as<String>().c_str(), NULL, 16);
|
||||
config.Dtu_PollInterval = root[F("dtu_pollinterval")].as<uint32_t>();
|
||||
config.Dtu_PaLevel = root[F("dtu_palevel")].as<uint8_t>();
|
||||
Configuration.write();
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
void WebApiEventlogClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -22,15 +22,21 @@ void WebApiEventlogClass::loop()
|
||||
|
||||
void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
uint64_t serial = 0;
|
||||
if (request->hasParam("inv")) {
|
||||
String s = request->getParam("inv")->value();
|
||||
serial = strtoll(s.c_str(), NULL, 16);
|
||||
}
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv != nullptr) {
|
||||
// Inverter Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
|
||||
@ -11,7 +11,12 @@
|
||||
|
||||
void WebApiFirmwareClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
void WebApiInverterClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -32,7 +32,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
JsonArray data = root.createNestedArray(F("inverter"));
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial > 0) {
|
||||
@ -42,7 +42,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
|
||||
// Inverter Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF)));
|
||||
obj[F("serial")] = buffer;
|
||||
@ -126,9 +126,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
char* t;
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
inverter->Serial = strtoll(root[F("serial")].as<String>().c_str(), &t, 16);
|
||||
inverter->Serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
|
||||
strncpy(inverter->Name, root[F("name")].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
Configuration.write();
|
||||
@ -224,8 +223,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
|
||||
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as<uint8_t>()];
|
||||
|
||||
char* t;
|
||||
uint64_t new_serial = strtoll(root[F("serial")].as<String>().c_str(), &t, 16);
|
||||
uint64_t new_serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
uint64_t old_serial = inverter.Serial;
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
|
||||
42
src/WebApi_limit.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_limit.h"
|
||||
#include "ArduinoJson.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Hoymiles.h"
|
||||
|
||||
void WebApiLimitClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1));
|
||||
}
|
||||
|
||||
void WebApiLimitClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
|
||||
// Inverter Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
root[buffer]["limit"] = inv->SystemConfigPara()->getLimitPercent();
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
void WebApiMqttClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -29,7 +29,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
|
||||
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
|
||||
@ -56,7 +56,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
|
||||
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
|
||||
@ -240,21 +240,21 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
config.Mqtt_Enabled = root[F("mqtt_enabled")].as<bool>();
|
||||
config.Mqtt_Retain = root[F("mqtt_retain")].as<bool>();
|
||||
config.Mqtt_Tls = root[F("mqtt_tls")].as<bool>();
|
||||
strcpy(config.Mqtt_RootCaCert, root[F("mqtt_root_ca_cert")].as<String>().c_str());
|
||||
strlcpy(config.Mqtt_RootCaCert, root[F("mqtt_root_ca_cert")].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert));
|
||||
config.Mqtt_Port = root[F("mqtt_port")].as<uint>();
|
||||
strcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str());
|
||||
strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str());
|
||||
strlcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str(), sizeof(config.Mqtt_Hostname));
|
||||
strlcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str(), sizeof(config.Mqtt_Username));
|
||||
strlcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str(), sizeof(config.Mqtt_Password));
|
||||
strlcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Mqtt_Topic));
|
||||
strlcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str(), sizeof(config.Mqtt_LwtTopic));
|
||||
strlcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Online));
|
||||
strlcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Offline));
|
||||
config.Mqtt_PublishInterval = root[F("mqtt_publish_interval")].as<uint32_t>();
|
||||
config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as<bool>();
|
||||
config.Mqtt_Hass_Expire = root[F("mqtt_hass_expire")].as<bool>();
|
||||
config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>();
|
||||
config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as<bool>();
|
||||
strcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str());
|
||||
strlcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str(), sizeof(config.Mqtt_Hass_Topic));
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
@ -267,18 +267,18 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
MqttHassPublishing.forceUpdate();
|
||||
}
|
||||
|
||||
String WebApiMqttClass::getRootCaCertInfo(char* cert)
|
||||
String WebApiMqttClass::getRootCaCertInfo(const char* cert)
|
||||
{
|
||||
char rootCaCertInfo[1024] = "";
|
||||
|
||||
mbedtls_x509_crt global_cacert;
|
||||
|
||||
strcpy(rootCaCertInfo, "Can't parse root ca");
|
||||
strlcpy(rootCaCertInfo, "Can't parse root ca", sizeof(rootCaCertInfo));
|
||||
|
||||
mbedtls_x509_crt_init(&global_cacert);
|
||||
int ret = mbedtls_x509_crt_parse(&global_cacert, const_cast<unsigned char*>((unsigned char*)cert), 1 + strlen(cert));
|
||||
if (ret < 0) {
|
||||
sprintf(rootCaCertInfo, "Can't parse root ca: mbedtls_x509_crt_parse returned -0x%x\n\n", -ret);
|
||||
snprintf(rootCaCertInfo, sizeof(rootCaCertInfo), "Can't parse root ca: mbedtls_x509_crt_parse returned -0x%x\n\n", -ret);
|
||||
mbedtls_x509_crt_free(&global_cacert);
|
||||
return "";
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
void WebApiNetworkClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -32,6 +32,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
|
||||
root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0);
|
||||
root[F("sta_ssid")] = WiFi.SSID();
|
||||
root[F("sta_rssi")] = WiFi.RSSI();
|
||||
root[F("network_hostname")] = NetworkSettings.getHostname();
|
||||
root[F("network_ip")] = NetworkSettings.localIP().toString();
|
||||
root[F("network_netmask")] = NetworkSettings.subnetMask().toString();
|
||||
root[F("network_gateway")] = NetworkSettings.gatewayIP().toString();
|
||||
@ -53,7 +54,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("hostname")] = config.WiFi_Hostname;
|
||||
root[F("dhcp")] = config.WiFi_Dhcp;
|
||||
@ -184,9 +185,9 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
config.WiFi_Dns2[1] = dns2[1];
|
||||
config.WiFi_Dns2[2] = dns2[2];
|
||||
config.WiFi_Dns2[3] = dns2[3];
|
||||
strcpy(config.WiFi_Ssid, root[F("ssid")].as<String>().c_str());
|
||||
strcpy(config.WiFi_Password, root[F("password")].as<String>().c_str());
|
||||
strcpy(config.WiFi_Hostname, root[F("hostname")].as<String>().c_str());
|
||||
strlcpy(config.WiFi_Ssid, root[F("ssid")].as<String>().c_str(), sizeof(config.WiFi_Ssid));
|
||||
strlcpy(config.WiFi_Password, root[F("password")].as<String>().c_str(), sizeof(config.WiFi_Password));
|
||||
strlcpy(config.WiFi_Hostname, root[F("hostname")].as<String>().c_str(), sizeof(config.WiFi_Hostname));
|
||||
if (root[F("dhcp")].as<bool>()) {
|
||||
config.WiFi_Dhcp = true;
|
||||
} else {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
void WebApiNtpClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -28,7 +28,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("ntp_server")] = config.Ntp_Server;
|
||||
root[F("ntp_timezone")] = config.Ntp_Timezone;
|
||||
@ -52,7 +52,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("ntp_server")] = config.Ntp_Server;
|
||||
root[F("ntp_timezone")] = config.Ntp_Timezone;
|
||||
@ -123,9 +123,9 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
strcpy(config.Ntp_Server, root[F("ntp_server")].as<String>().c_str());
|
||||
strcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as<String>().c_str());
|
||||
strcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as<String>().c_str());
|
||||
strlcpy(config.Ntp_Server, root[F("ntp_server")].as<String>().c_str(), sizeof(config.Ntp_Server));
|
||||
strlcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as<String>().c_str(), sizeof(config.Ntp_Timezone));
|
||||
strlcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as<String>().c_str(), sizeof(config.Ntp_TimezoneDescr));
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
|
||||
void WebApiSysstatusClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
@ -58,7 +58,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
|
||||
root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount;
|
||||
|
||||
char version[16];
|
||||
sprintf(version, "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
|
||||
snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
|
||||
root[F("firmware_version")] = version;
|
||||
root[F("git_hash")] = AUTO_GIT_HASH;
|
||||
|
||||
|
||||
@ -16,8 +16,6 @@ extern const uint8_t file_app_js_end[] asm("_binary_webapp_dist_js_app_js_gz_end
|
||||
|
||||
void WebApiWebappClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
|
||||
|
||||
@ -13,7 +13,12 @@ WebApiWsLiveClass::WebApiWsLiveClass()
|
||||
|
||||
void WebApiWsLiveClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
_server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1));
|
||||
@ -56,10 +61,9 @@ void WebApiWsLiveClass::loop()
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
|
||||
size_t len = measureJson(root);
|
||||
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); // creates a buffer (len + 1) for you.
|
||||
String buffer;
|
||||
if (buffer) {
|
||||
serializeJson(root, (char*)buffer->get(), len + 1);
|
||||
serializeJson(root, buffer);
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
|
||||
@ -74,7 +78,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
sprintf(buffer, "%0lx%08lx",
|
||||
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
|
||||
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
|
||||
|
||||
@ -135,11 +139,11 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
sprintf(str, "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
sprintf(str, "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ void setup()
|
||||
|
||||
// Initialize inverter communication
|
||||
Serial.print(F("Initialize Hoymiles interface... "));
|
||||
CONFIG_T& config = Configuration.get();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
Hoymiles.init();
|
||||
|
||||
Serial.println(F(" Setting radio PA level... "));
|
||||
|
||||
@ -9,30 +9,30 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap": "^5.2.1",
|
||||
"bootstrap-icons-vue": "^1.8.1",
|
||||
"core-js": "^3.25.0",
|
||||
"core-js": "^3.25.1",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue": "^3.2.38",
|
||||
"vue": "^3.2.39",
|
||||
"vue-class-component": "^8.0.0-0",
|
||||
"vue-router": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/core": "^7.19.0",
|
||||
"@babel/eslint-parser": "^7.18.9",
|
||||
"@types/bootstrap": "^5.2.2",
|
||||
"@types/node": "^18.7.15",
|
||||
"@types/bootstrap": "^5.2.4",
|
||||
"@types/node": "^18.7.16",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"@vue/cli-plugin-babel": "~5.0.8",
|
||||
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||
"@vue/cli-plugin-router": "^5.0.6",
|
||||
"@vue/cli-plugin-typescript": "^5.0.8",
|
||||
"@vue/cli-service": "~5.0.8",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"eslint": "^8.23.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.1",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-vue": "^9.4.0",
|
||||
"typescript": "^4.8.2",
|
||||
"typescript": "^4.8.3",
|
||||
"vue-cli-plugin-compression": "~2.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@ -1,10 +1,101 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>About</h1>
|
||||
This project was started from
|
||||
<a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion.
|
||||
(Mikrocontroller.net)</a>
|
||||
<h1>About OpenDTU</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="accordion" id="accordionExample">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
|
||||
<span class="badge bg-secondary">
|
||||
<BIconInfoCircle class="fs-4" />
|
||||
</span> Project Origin
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne"
|
||||
data-bs-parent="#accordionExample">
|
||||
<div class="accordion-body">
|
||||
<p class="fw-normal">
|
||||
This project was started from
|
||||
<a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion.
|
||||
(Mikrocontroller.net)</a>
|
||||
</p>
|
||||
<p class="fw-normal">
|
||||
The Hoymiles protocol was decrypted through the voluntary efforts of many participants.
|
||||
OpenDTU,
|
||||
among others, was developed based on this work. The project is licensed under an Open Source
|
||||
License (<a href="https://www.gnu.de/documents/gpl-2.0.de.html" target="_blank">GNU General
|
||||
Public License version 2</a>).
|
||||
</p>
|
||||
<p class="fw-normal">
|
||||
The software was developed to the best of our knowledge and belief. Nevertheless, no
|
||||
liability
|
||||
can be accepted for a malfunction or guarantee loss of the inverter.
|
||||
</p>
|
||||
<p class="fw-normal">
|
||||
OpenDTU is freely available. If you paid money for the software, you probably got ripped
|
||||
off.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingTwo">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||
<span class="badge bg-secondary">
|
||||
<BIconActivity class="fs-4" />
|
||||
</span> News & Updates
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo"
|
||||
data-bs-parent="#accordionExample">
|
||||
<div class="accordion-body">
|
||||
New updates can be found on Github: <a href="https://github.com/tbnobody/OpenDTU"
|
||||
target="_blank">https://github.com/tbnobody/OpenDTU</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingThree">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||
<span class="badge bg-secondary">
|
||||
<BIconBug class="fs-4" />
|
||||
</span> Error Reporting
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree"
|
||||
data-bs-parent="#accordionExample">
|
||||
<div class="accordion-body">
|
||||
Please report issues using the feature provided by <a
|
||||
href="https://github.com/tbnobody/OpenDTU/issues" target="_blank">Github</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingFour">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||
<span class="badge bg-secondary">
|
||||
<BIconChat class="fs-4" />
|
||||
</span> Discussion
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour"
|
||||
data-bs-parent="#accordionExample">
|
||||
<div class="accordion-body">
|
||||
Discuss with us on <a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a> or <a
|
||||
href="https://github.com/tbnobody/OpenDTU/discussions" target="_blank">Github</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
219
webapp/src/components/ConfigAdminView.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div class="container-xxl" role="main">
|
||||
<div class="page-header">
|
||||
<h1>Config Management</h1>
|
||||
</div>
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<div class="text-center" v-if="loading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="!loading">
|
||||
<div class="card">
|
||||
<div class="card-header text-white bg-primary">Backup: Configuration File Backup</div>
|
||||
<div class="card-body text-center">
|
||||
Backup the configuration file
|
||||
<button class="btn btn-primary" @click="downloadConfig">Backup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Restore: Restore the Configuration File</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<div v-if="!uploading && UploadError != ''">
|
||||
<p class="h1 mb-2">
|
||||
<BIconExclamationCircleFill />
|
||||
</p>
|
||||
<span style="vertical-align: middle" class="ml-2">
|
||||
{{ UploadError }}
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-light" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading && UploadSuccess">
|
||||
<span class="h1 mb-2">
|
||||
<BIconCheckCircle />
|
||||
</span>
|
||||
<span> Upload Success </span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-primary" @click="clear">
|
||||
<BIconArrowLeft /> Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading">
|
||||
<div class="form-group pt-2 mt-3">
|
||||
<input class="form-control" type="file" ref="file" accept=".bin" @change="uploadConfig" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="uploading">
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" :style="{ width: progress + '%' }"
|
||||
v-bind:aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
|
||||
{{ progress }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> This operation replaces the configuration file with the restored configuration and
|
||||
restarts OpenDTU to apply all settings.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-5">
|
||||
<div class="card-header text-white bg-primary">Initialize: Perform Factory Reset</div>
|
||||
<div class="card-body text-center">
|
||||
|
||||
<button class="btn btn-danger" @click="onFactoryResetModal">Restore Factory-Default Settings
|
||||
</button>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
<b>Note:</b> Click Restore Factory-Default Settings to restore and initialize the
|
||||
factory-default settings and reboot.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="modal" id="factoryReset" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Factory Reset</h5>
|
||||
<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 current configuration and reset all settings to their
|
||||
factory defaults?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onFactoryResetCancel"
|
||||
data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" @click="onFactoryResetPerform">Factory
|
||||
Reset!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import BootstrapAlert from "@/components/partials/BootstrapAlert.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BootstrapAlert,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalFactoryReset: {} as bootstrap.Modal,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
loading: true,
|
||||
uploading: false,
|
||||
progress: 0,
|
||||
UploadError: "",
|
||||
UploadSuccess: false,
|
||||
file: {} as Blob,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modalFactoryReset = new bootstrap.Modal('#factoryReset');
|
||||
this.loading = false;
|
||||
},
|
||||
methods: {
|
||||
onFactoryResetModal() {
|
||||
this.modalFactoryReset.show();
|
||||
},
|
||||
onFactoryResetCancel() {
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
onFactoryResetPerform() {
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify({ delete: true }));
|
||||
|
||||
fetch("/api/config/delete", {
|
||||
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;
|
||||
}
|
||||
)
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
downloadConfig() {
|
||||
const link = document.createElement('a')
|
||||
link.href = "/api/config/get"
|
||||
link.download = 'config.bin'
|
||||
link.click()
|
||||
},
|
||||
uploadConfig(event: Event | null) {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
if (event !== null) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files !== null) {
|
||||
this.file = target.files[0];
|
||||
}
|
||||
}
|
||||
const request = new XMLHttpRequest();
|
||||
request.addEventListener("load", () => {
|
||||
// request.response will hold the response from the server
|
||||
if (request.status === 200) {
|
||||
this.UploadSuccess = true;
|
||||
} else if (request.status !== 500) {
|
||||
this.UploadError = `[HTTP ERROR] ${request.statusText}`;
|
||||
} else {
|
||||
this.UploadError = request.responseText;
|
||||
}
|
||||
this.uploading = false;
|
||||
this.progress = 0;
|
||||
});
|
||||
// Upload progress
|
||||
request.upload.addEventListener("progress", (e) => {
|
||||
this.progress = Math.trunc((e.loaded / e.total) * 100);
|
||||
});
|
||||
request.withCredentials = true;
|
||||
|
||||
formData.append("config", this.file, "config");
|
||||
request.open("post", "/api/config/upload");
|
||||
request.send(formData);
|
||||
},
|
||||
clear() {
|
||||
this.UploadError = "";
|
||||
this.UploadSuccess = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -39,6 +39,14 @@
|
||||
{{ inverter.data_age }} seconds)
|
||||
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-sm btn-info"
|
||||
@click="onShowDevInfo(inverter.serial)" title="Show Inverter Info">
|
||||
@ -130,6 +138,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="limitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Limit Settings</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center" v-if="limitSettingLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LimitSettingsCurrent v-if="!limitSettingLoading" :limitData="limitSettingList" />
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="onHideLimitSettings"
|
||||
data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -139,6 +171,7 @@ import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import EventLog from '@/components/partials/EventLog.vue';
|
||||
import DevInfo from '@/components/partials/DevInfo.vue';
|
||||
import LimitSettingsCurrent from '@/components/partials/LimitSettingsCurrent.vue';
|
||||
import VedirectView from '@/components/partials/VedirectView.vue';
|
||||
|
||||
declare interface Inverter {
|
||||
@ -154,6 +187,7 @@ export default defineComponent({
|
||||
InverterChannelInfo,
|
||||
EventLog,
|
||||
DevInfo,
|
||||
LimitSettingsCurrent,
|
||||
VedirectView
|
||||
},
|
||||
data() {
|
||||
@ -169,7 +203,10 @@ export default defineComponent({
|
||||
eventLogLoading: true,
|
||||
devInfoView: {} as bootstrap.Modal,
|
||||
devInfoList: {},
|
||||
devInfoLoading: true
|
||||
devInfoLoading: true,
|
||||
limitSettingView: {} as bootstrap.Modal,
|
||||
limitSettingList: {},
|
||||
limitSettingLoading: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@ -180,6 +217,7 @@ export default defineComponent({
|
||||
mounted() {
|
||||
this.eventLogView = new bootstrap.Modal('#eventView');
|
||||
this.devInfoView = new bootstrap.Modal('#devInfoView');
|
||||
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
@ -264,7 +302,7 @@ export default defineComponent({
|
||||
},
|
||||
onShowEventlog(serial: number) {
|
||||
this.eventLogLoading = true;
|
||||
fetch("/api/eventlog/status")
|
||||
fetch("/api/eventlog/status?inv=" + serial)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.eventLogList = data[serial];
|
||||
@ -287,6 +325,20 @@ export default defineComponent({
|
||||
|
||||
this.devInfoView.show();
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.limitSettingView.hide();
|
||||
},
|
||||
onShowLimitSettings(serial: number) {
|
||||
this.limitSettingLoading = true;
|
||||
fetch("/api/limit/status")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.limitSettingList = data[serial];
|
||||
this.limitSettingLoading = false;
|
||||
});
|
||||
|
||||
this.limitSettingView.show();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -39,6 +39,9 @@
|
||||
<li>
|
||||
<hr class="dropdown-divider" />
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/config">Config Management</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/firmware/upgrade">Firmware Upgrade</router-link>
|
||||
</li>
|
||||
|
||||
@ -39,6 +39,11 @@
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="inputHostname" maxlength="32"
|
||||
placeholder="Hostname" v-model="networkConfigList.hostname" />
|
||||
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
<b>Hint:</b> The text <span class="font-monospace">%06X</span> will be replaced
|
||||
with the last 6 digits of the ESP ChipID in hex format.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ export default defineComponent({
|
||||
ap_ssid: "",
|
||||
ap_stationnum: 0,
|
||||
// InterfaceNetworkInfo
|
||||
network_hostname: "",
|
||||
network_ip: "",
|
||||
network_netmask: "",
|
||||
network_gateway: "",
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<td>{{ network_hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<td>{{ network_ip }}</td>
|
||||
@ -43,6 +47,7 @@ import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
network_hostname: String,
|
||||
network_ip: String,
|
||||
network_netmask: String,
|
||||
network_gateway: String,
|
||||
|
||||
33
webapp/src/components/partials/LimitSettingsCurrent.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<table class="table table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Current Limit</td>
|
||||
<td>{{ formatNumber(limitData.limit) }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
declare interface LimitData {
|
||||
limit: number,
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
limitData: { type: Object as () => LimitData, required: true },
|
||||
},
|
||||
computed: {
|
||||
formatNumber() {
|
||||
return (num: number) => {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num)
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -11,6 +11,7 @@ import MqttInfoView from '@/components/MqttInfoView.vue'
|
||||
import InverterAdminView from '@/components/InverterAdminView.vue'
|
||||
import DtuAdminView from '@/components/DtuAdminView.vue'
|
||||
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
|
||||
import ConfigAdminView from '@/components/ConfigAdminView.vue'
|
||||
import VedirectAdminView from '@/components/VedirectAdminView.vue'
|
||||
import VedirectInfoView from '@/components/VedirectInfoView.vue'
|
||||
|
||||
@ -84,6 +85,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: '/firmware/upgrade',
|
||||
name: 'Firmware Upgrade',
|
||||
component: FirmwareUpgradeView
|
||||
},
|
||||
{
|
||||
path: '/settings/config',
|
||||
name: 'Config Management',
|
||||
component: ConfigAdminView
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
375
webapp/yarn.lock
@ -36,6 +36,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
|
||||
integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==
|
||||
|
||||
"@babel/compat-data@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.0.tgz#2a592fd89bacb1fcde68de31bee4f2f2dacb0e86"
|
||||
integrity sha512-y5rqgTTPTmaF5e2nVhOxw+Ur9HDJLsWb6U/KpgUzRZEdPfE6VOubXBKLdbcUTijzRptednSBDQbYZBOSqJxpJw==
|
||||
|
||||
"@babel/core@^7.12.16":
|
||||
version "7.18.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8"
|
||||
@ -57,21 +62,21 @@
|
||||
json5 "^2.2.1"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@babel/core@^7.18.13":
|
||||
version "7.18.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac"
|
||||
integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==
|
||||
"@babel/core@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.0.tgz#d2f5f4f2033c00de8096be3c9f45772563e150c3"
|
||||
integrity sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.1.0"
|
||||
"@babel/code-frame" "^7.18.6"
|
||||
"@babel/generator" "^7.18.13"
|
||||
"@babel/helper-compilation-targets" "^7.18.9"
|
||||
"@babel/helper-module-transforms" "^7.18.9"
|
||||
"@babel/helpers" "^7.18.9"
|
||||
"@babel/parser" "^7.18.13"
|
||||
"@babel/generator" "^7.19.0"
|
||||
"@babel/helper-compilation-targets" "^7.19.0"
|
||||
"@babel/helper-module-transforms" "^7.19.0"
|
||||
"@babel/helpers" "^7.19.0"
|
||||
"@babel/parser" "^7.19.0"
|
||||
"@babel/template" "^7.18.10"
|
||||
"@babel/traverse" "^7.18.13"
|
||||
"@babel/types" "^7.18.13"
|
||||
"@babel/traverse" "^7.19.0"
|
||||
"@babel/types" "^7.19.0"
|
||||
convert-source-map "^1.7.0"
|
||||
debug "^4.1.0"
|
||||
gensync "^1.0.0-beta.2"
|
||||
@ -96,12 +101,12 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.2"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/generator@^7.18.13":
|
||||
version "7.18.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212"
|
||||
integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==
|
||||
"@babel/generator@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.0.tgz#785596c06425e59334df2ccee63ab166b738419a"
|
||||
integrity sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.18.13"
|
||||
"@babel/types" "^7.19.0"
|
||||
"@jridgewell/gen-mapping" "^0.3.2"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
@ -130,6 +135,16 @@
|
||||
browserslist "^4.20.2"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.0.tgz#537ec8339d53e806ed422f1e06c8f17d55b96bb0"
|
||||
integrity sha512-Ai5bNWXIvwDvWM7njqsG3feMlL9hCVQsPYXodsZyLwshYkZVJt59Gftau4VrE8S9IT9asd2uSP1hG6wCNw+sXA==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.19.0"
|
||||
"@babel/helper-validator-option" "^7.18.6"
|
||||
browserslist "^4.20.2"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.17.12", "@babel/helper-create-class-features-plugin@^7.18.0":
|
||||
version "7.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.0.tgz#fac430912606331cb075ea8d82f9a4c145a4da19"
|
||||
@ -198,6 +213,14 @@
|
||||
"@babel/template" "^7.18.6"
|
||||
"@babel/types" "^7.18.9"
|
||||
|
||||
"@babel/helper-function-name@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
|
||||
integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
|
||||
dependencies:
|
||||
"@babel/template" "^7.18.10"
|
||||
"@babel/types" "^7.19.0"
|
||||
|
||||
"@babel/helper-hoist-variables@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246"
|
||||
@ -247,6 +270,20 @@
|
||||
"@babel/traverse" "^7.18.9"
|
||||
"@babel/types" "^7.18.9"
|
||||
|
||||
"@babel/helper-module-transforms@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz#309b230f04e22c58c6a2c0c0c7e50b216d350c30"
|
||||
integrity sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==
|
||||
dependencies:
|
||||
"@babel/helper-environment-visitor" "^7.18.9"
|
||||
"@babel/helper-module-imports" "^7.18.6"
|
||||
"@babel/helper-simple-access" "^7.18.6"
|
||||
"@babel/helper-split-export-declaration" "^7.18.6"
|
||||
"@babel/helper-validator-identifier" "^7.18.6"
|
||||
"@babel/template" "^7.18.10"
|
||||
"@babel/traverse" "^7.19.0"
|
||||
"@babel/types" "^7.19.0"
|
||||
|
||||
"@babel/helper-optimise-call-expression@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2"
|
||||
@ -358,6 +395,15 @@
|
||||
"@babel/traverse" "^7.18.9"
|
||||
"@babel/types" "^7.18.9"
|
||||
|
||||
"@babel/helpers@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.0.tgz#f30534657faf246ae96551d88dd31e9d1fa1fc18"
|
||||
integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==
|
||||
dependencies:
|
||||
"@babel/template" "^7.18.10"
|
||||
"@babel/traverse" "^7.19.0"
|
||||
"@babel/types" "^7.19.0"
|
||||
|
||||
"@babel/highlight@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
|
||||
@ -372,10 +418,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9"
|
||||
integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==
|
||||
|
||||
"@babel/parser@^7.18.13":
|
||||
version "7.18.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
|
||||
integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
|
||||
"@babel/parser@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c"
|
||||
integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw==
|
||||
|
||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.17.12":
|
||||
version "7.17.12"
|
||||
@ -1040,19 +1086,19 @@
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
"@babel/traverse@^7.18.13":
|
||||
version "7.18.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68"
|
||||
integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==
|
||||
"@babel/traverse@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.0.tgz#eb9c561c7360005c592cc645abafe0c3c4548eed"
|
||||
integrity sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.18.6"
|
||||
"@babel/generator" "^7.18.13"
|
||||
"@babel/generator" "^7.19.0"
|
||||
"@babel/helper-environment-visitor" "^7.18.9"
|
||||
"@babel/helper-function-name" "^7.18.9"
|
||||
"@babel/helper-function-name" "^7.19.0"
|
||||
"@babel/helper-hoist-variables" "^7.18.6"
|
||||
"@babel/helper-split-export-declaration" "^7.18.6"
|
||||
"@babel/parser" "^7.18.13"
|
||||
"@babel/types" "^7.18.13"
|
||||
"@babel/parser" "^7.19.0"
|
||||
"@babel/types" "^7.19.0"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
@ -1065,19 +1111,19 @@
|
||||
"@babel/helper-validator-identifier" "^7.18.6"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.18.13":
|
||||
version "7.18.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
|
||||
integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==
|
||||
"@babel/types@^7.19.0":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600"
|
||||
integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.18.10"
|
||||
"@babel/helper-validator-identifier" "^7.18.6"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@eslint/eslintrc@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d"
|
||||
integrity sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==
|
||||
"@eslint/eslintrc@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356"
|
||||
integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==
|
||||
dependencies:
|
||||
ajv "^6.12.4"
|
||||
debug "^4.3.2"
|
||||
@ -1270,10 +1316,10 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/bootstrap@^5.2.2":
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.3.tgz#562defe61dac6c9598843d4088de8007a36607bd"
|
||||
integrity sha512-r2SE9NYaaI7B/jJk8gqRtXzlhgFL6dlXBResJkCbQa8ept619WeiOIO4zBQxdmUFzkKNWLK5ZOyYGI3QZoaqbQ==
|
||||
"@types/bootstrap@^5.2.4":
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.4.tgz#7f4f4af8e22af8247385549bd2f687088d00d2d3"
|
||||
integrity sha512-jGNB81zuDHu1DPvBV7Ox3Z3eyzdWPNguYwrt0j7X90VExA8H7c6qxJh0cz5j3xp0XvSy1TYaP2pkyXCHeo8CaA==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
|
||||
@ -1372,10 +1418,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
|
||||
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
|
||||
|
||||
"@types/node@^18.7.15":
|
||||
version "18.7.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.15.tgz#20ae1ec80c57ee844b469f968a1cd511d4088b29"
|
||||
integrity sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ==
|
||||
"@types/node@^18.7.16":
|
||||
version "18.7.16"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.16.tgz#0eb3cce1e37c79619943d2fd903919fc30850601"
|
||||
integrity sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.1"
|
||||
@ -1466,14 +1512,14 @@
|
||||
"@typescript-eslint/typescript-estree" "5.32.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/parser@^5.36.1":
|
||||
version "5.36.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.1.tgz#931c22c7bacefd17e29734628cdec8b2acdcf1ce"
|
||||
integrity sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==
|
||||
"@typescript-eslint/parser@^5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.2.tgz#3ddf323d3ac85a25295a55fcb9c7a49ab4680ddd"
|
||||
integrity sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.36.1"
|
||||
"@typescript-eslint/types" "5.36.1"
|
||||
"@typescript-eslint/typescript-estree" "5.36.1"
|
||||
"@typescript-eslint/scope-manager" "5.36.2"
|
||||
"@typescript-eslint/types" "5.36.2"
|
||||
"@typescript-eslint/typescript-estree" "5.36.2"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.29.0":
|
||||
@ -1492,13 +1538,13 @@
|
||||
"@typescript-eslint/types" "5.32.0"
|
||||
"@typescript-eslint/visitor-keys" "5.32.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.36.1":
|
||||
version "5.36.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.1.tgz#23c49b7ddbcffbe09082e6694c2524950766513f"
|
||||
integrity sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==
|
||||
"@typescript-eslint/scope-manager@5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz#a75eb588a3879ae659514780831370642505d1cd"
|
||||
integrity sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.36.1"
|
||||
"@typescript-eslint/visitor-keys" "5.36.1"
|
||||
"@typescript-eslint/types" "5.36.2"
|
||||
"@typescript-eslint/visitor-keys" "5.36.2"
|
||||
|
||||
"@typescript-eslint/type-utils@5.29.0":
|
||||
version "5.29.0"
|
||||
@ -1519,10 +1565,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.32.0.tgz#484273021eeeae87ddb288f39586ef5efeb6dcd8"
|
||||
integrity sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==
|
||||
|
||||
"@typescript-eslint/types@5.36.1":
|
||||
version "5.36.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.1.tgz#1cf0e28aed1cb3ee676917966eb23c2f8334ce2c"
|
||||
integrity sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==
|
||||
"@typescript-eslint/types@5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.2.tgz#a5066e500ebcfcee36694186ccc57b955c05faf9"
|
||||
integrity sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.29.0":
|
||||
version "5.29.0"
|
||||
@ -1550,13 +1596,13 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.36.1":
|
||||
version "5.36.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.1.tgz#b857f38d6200f7f3f4c65cd0a5afd5ae723f2adb"
|
||||
integrity sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==
|
||||
"@typescript-eslint/typescript-estree@5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz#0c93418b36c53ba0bc34c61fe9405c4d1d8fe560"
|
||||
integrity sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.36.1"
|
||||
"@typescript-eslint/visitor-keys" "5.36.1"
|
||||
"@typescript-eslint/types" "5.36.2"
|
||||
"@typescript-eslint/visitor-keys" "5.36.2"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
@ -1591,12 +1637,12 @@
|
||||
"@typescript-eslint/types" "5.32.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.36.1":
|
||||
version "5.36.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.1.tgz#7731175312d65738e501780f923896d200ad1615"
|
||||
integrity sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==
|
||||
"@typescript-eslint/visitor-keys@5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz#2f8f78da0a3bad3320d2ac24965791ac39dace5a"
|
||||
integrity sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.36.1"
|
||||
"@typescript-eslint/types" "5.36.2"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
|
||||
@ -1855,47 +1901,47 @@
|
||||
semver "^7.3.4"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"@vue/compiler-core@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7"
|
||||
integrity sha512-/FsvnSu7Z+lkd/8KXMa4yYNUiqQrI22135gfsQYVGuh5tqEgOB0XqrUdb/KnCLa5+TmQLPwvyUnKMyCpu+SX3Q==
|
||||
"@vue/compiler-core@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.39.tgz#0d77e635f4bdb918326669155a2dc977c053943e"
|
||||
integrity sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/shared" "3.2.39"
|
||||
estree-walker "^2.0.2"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-dom@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.38.tgz#53d04ed0c0c62d1ef259bf82f9b28100a880b6fd"
|
||||
integrity sha512-zqX4FgUbw56kzHlgYuEEJR8mefFiiyR3u96498+zWPsLeh1WKvgIReoNE+U7gG8bCUdvsrJ0JRmev0Ky6n2O0g==
|
||||
"@vue/compiler-dom@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz#bd69d35c1a48fe2cea4ab9e96d2a3a735d146fdf"
|
||||
integrity sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-core" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/compiler-sfc@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.38.tgz#9e763019471a535eb1fceeaac9d4d18a83f0940f"
|
||||
integrity sha512-KZjrW32KloMYtTcHAFuw3CqsyWc5X6seb8KbkANSWt3Cz9p2qA8c1GJpSkksFP9ABb6an0FLCFl46ZFXx3kKpg==
|
||||
"@vue/compiler-sfc@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz#8fe29990f672805b7c5a2ecfa5b05e681c862ea2"
|
||||
integrity sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.38"
|
||||
"@vue/compiler-dom" "3.2.38"
|
||||
"@vue/compiler-ssr" "3.2.38"
|
||||
"@vue/reactivity-transform" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-core" "3.2.39"
|
||||
"@vue/compiler-dom" "3.2.39"
|
||||
"@vue/compiler-ssr" "3.2.39"
|
||||
"@vue/reactivity-transform" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
postcss "^8.1.10"
|
||||
source-map "^0.6.1"
|
||||
|
||||
"@vue/compiler-ssr@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.38.tgz#933b23bf99e667e5078eefc6ba94cb95fd765dfe"
|
||||
integrity sha512-bm9jOeyv1H3UskNm4S6IfueKjUNFmi2kRweFIGnqaGkkRePjwEcfCVqyS3roe7HvF4ugsEkhf4+kIvDhip6XzQ==
|
||||
"@vue/compiler-ssr@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz#4f3bfb535cb98b764bee45e078700e03ccc60633"
|
||||
integrity sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-dom" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.3.0":
|
||||
version "3.3.0"
|
||||
@ -1918,62 +1964,62 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
|
||||
integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
|
||||
|
||||
"@vue/eslint-config-typescript@^11.0.0":
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.0.tgz#bac0cb2d381625b5bf568d2025acffc0fd09113e"
|
||||
integrity sha512-txuRzxnQVmtUvvy9UyWUy9sHWXNeRPGmSPqP53hRtaiUeCTAondI9Ho9GQYI/8/eWljYOST7iA4Aa8sANBkWaA==
|
||||
"@vue/eslint-config-typescript@^11.0.1":
|
||||
version "11.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.1.tgz#d79b3656aecea844ec9875bc93155163f684dde7"
|
||||
integrity sha512-0U+nL0nA7ahnGPk3rTN49x76miUwuQtQPQNWOFvAcjg6nFJkIkA8qbGNtXwsuHtwBwRtWpHhShL3zK07v+632w==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "^5.0.0"
|
||||
"@typescript-eslint/parser" "^5.0.0"
|
||||
vue-eslint-parser "^9.0.0"
|
||||
|
||||
"@vue/reactivity-transform@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.38.tgz#a856c217b2ead99eefb6fddb1d61119b2cb67984"
|
||||
integrity sha512-3SD3Jmi1yXrDwiNJqQ6fs1x61WsDLqVk4NyKVz78mkaIRh6d3IqtRnptgRfXn+Fzf+m6B1KxBYWq1APj6h4qeA==
|
||||
"@vue/reactivity-transform@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz#da6ae6c8fd77791b9ae21976720d116591e1c4aa"
|
||||
integrity sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.16.4"
|
||||
"@vue/compiler-core" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-core" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.25.7"
|
||||
|
||||
"@vue/reactivity@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.38.tgz#d576fdcea98eefb96a1f1ad456e289263e87292e"
|
||||
integrity sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==
|
||||
"@vue/reactivity@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.39.tgz#e6e3615fe2288d4232b104640ddabd0729a78c80"
|
||||
integrity sha512-vlaYX2a3qMhIZfrw3Mtfd+BuU+TZmvDrPMa+6lpfzS9k/LnGxkSuf0fhkP0rMGfiOHPtyKoU9OJJJFGm92beVQ==
|
||||
dependencies:
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/runtime-core@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.38.tgz#d19cf591c210713f80e6a94ffbfef307c27aea06"
|
||||
integrity sha512-kk0qiSiXUU/IKxZw31824rxmFzrLr3TL6ZcbrxWTKivadoKupdlzbQM4SlGo4MU6Zzrqv4fzyUasTU1jDoEnzg==
|
||||
"@vue/runtime-core@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.39.tgz#dc1faccab11b3e81197aba33fb30c9447c1d2c84"
|
||||
integrity sha512-xKH5XP57JW5JW+8ZG1khBbuLakINTgPuINKL01hStWLTTGFOrM49UfCFXBcFvWmSbci3gmJyLl2EAzCaZWsx8g==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/reactivity" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/runtime-dom@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.38.tgz#fec711f65c2485991289fd4798780aa506469b48"
|
||||
integrity sha512-4PKAb/ck2TjxdMSzMsnHViOrrwpudk4/A56uZjhzvusoEU9xqa5dygksbzYepdZeB5NqtRw5fRhWIiQlRVK45A==
|
||||
"@vue/runtime-dom@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.39.tgz#4a8cb132bcef316e8151c5ed07fc7272eb064614"
|
||||
integrity sha512-4G9AEJP+sLhsqf5wXcyKVWQKUhI+iWfy0hWQgea+CpaTD7BR0KdQzvoQdZhwCY6B3oleSyNLkLAQwm0ya/wNoA==
|
||||
dependencies:
|
||||
"@vue/runtime-core" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/runtime-core" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
csstype "^2.6.8"
|
||||
|
||||
"@vue/server-renderer@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.38.tgz#01a4c0f218e90b8ad1815074208a1974ded109aa"
|
||||
integrity sha512-pg+JanpbOZ5kEfOZzO2bt02YHd+ELhYP8zPeLU1H0e7lg079NtuuSB8fjLdn58c4Ou8UQ6C1/P+528nXnLPAhA==
|
||||
"@vue/server-renderer@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.39.tgz#4358292d925233b0d8b54cf0513eaece8b2351c5"
|
||||
integrity sha512-1yn9u2YBQWIgytFMjz4f/t0j43awKytTGVptfd3FtBk76t1pd8mxbek0G/DrnjJhd2V7mSTb5qgnxMYt8Z5iSQ==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-ssr" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
"@vue/shared@3.2.38":
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.38.tgz#e823f0cb2e85b6bf43430c0d6811b1441c300f3c"
|
||||
integrity sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==
|
||||
"@vue/shared@3.2.39":
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.39.tgz#302df167559a1a5156da162d8cc6760cef67f8e3"
|
||||
integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
|
||||
|
||||
"@vue/vue-loader-v15@npm:vue-loader@^15.9.7":
|
||||
version "15.9.8"
|
||||
@ -2413,10 +2459,10 @@ bootstrap-icons-vue@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.8.1.tgz#ce4a0c1f6efe41dabcc1341f2cb191d307fbaf50"
|
||||
integrity sha512-uItRULwQz0epETi9x/RBEqfjHmTAmkIIczpH1R6L9T6yyaaijk0826PzTWnWNm15tw66JT/8GNuXjB0HI5PHLw==
|
||||
|
||||
bootstrap@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.0.tgz#838727fb60f1630db370fe57c63cbcf2962bb3d3"
|
||||
integrity sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A==
|
||||
bootstrap@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.1.tgz#45f97ff05cbe828bad807b014d8425f3aeb8ec3a"
|
||||
integrity sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
@ -2791,10 +2837,10 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.8.3:
|
||||
browserslist "^4.20.3"
|
||||
semver "7.0.0"
|
||||
|
||||
core-js@^3.25.0:
|
||||
version "3.25.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.0.tgz#be71d9e0dd648ffd70c44a7ec2319d039357eceb"
|
||||
integrity sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA==
|
||||
core-js@^3.25.1:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.1.tgz#5818e09de0db8956e16bf10e2a7141e931b7c69c"
|
||||
integrity sha512-sr0FY4lnO1hkQ4gLDr24K0DGnweGO1QwSj5BpfQjpSJPdqWalja4cTps29Y/PJVG/P7FYlPDkH3hO+Tr0CvDgQ==
|
||||
|
||||
core-js@^3.8.3:
|
||||
version "3.24.1"
|
||||
@ -3293,12 +3339,12 @@ eslint-webpack-plugin@^3.1.0:
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^3.1.1"
|
||||
|
||||
eslint@^8.23.0:
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.0.tgz#a184918d288820179c6041bb3ddcc99ce6eea040"
|
||||
integrity sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==
|
||||
eslint@^8.23.1:
|
||||
version "8.23.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.1.tgz#cfd7b3f7fdd07db8d16b4ac0516a29c8d8dca5dc"
|
||||
integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.1"
|
||||
"@eslint/eslintrc" "^1.3.2"
|
||||
"@humanwhocodes/config-array" "^0.10.4"
|
||||
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
@ -3317,7 +3363,6 @@ eslint@^8.23.0:
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
find-up "^5.0.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob-parent "^6.0.1"
|
||||
globals "^13.15.0"
|
||||
globby "^11.1.0"
|
||||
@ -3326,6 +3371,7 @@ eslint@^8.23.0:
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
js-sdsl "^4.1.4"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
@ -4168,6 +4214,11 @@ js-message@1.0.7:
|
||||
resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47"
|
||||
integrity sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6"
|
||||
integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -6097,10 +6148,10 @@ type-is@~1.6.18:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typescript@^4.8.2:
|
||||
version "4.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
|
||||
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
|
||||
typescript@^4.8.3:
|
||||
version "4.8.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88"
|
||||
integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.0"
|
||||
@ -6234,16 +6285,16 @@ vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue@^3.2.38:
|
||||
version "3.2.38"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.38.tgz#cda3a414631745b194971219318a792dbbccdec0"
|
||||
integrity sha512-hHrScEFSmDAWL0cwO4B6WO7D3sALZPbfuThDsGBebthrNlDxdJZpGR3WB87VbjpPh96mep1+KzukYEhpHDFa8Q==
|
||||
vue@^3.2.39:
|
||||
version "3.2.39"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.39.tgz#de071c56c4c32c41cbd54e55f11404295c0dd62d"
|
||||
integrity sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.2.38"
|
||||
"@vue/compiler-sfc" "3.2.38"
|
||||
"@vue/runtime-dom" "3.2.38"
|
||||
"@vue/server-renderer" "3.2.38"
|
||||
"@vue/shared" "3.2.38"
|
||||
"@vue/compiler-dom" "3.2.39"
|
||||
"@vue/compiler-sfc" "3.2.39"
|
||||
"@vue/runtime-dom" "3.2.39"
|
||||
"@vue/server-renderer" "3.2.39"
|
||||
"@vue/shared" "3.2.39"
|
||||
|
||||
watchpack@^2.3.1:
|
||||
version "2.3.1"
|
||||
|
||||