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

This commit is contained in:
helgeerbe 2022-09-19 10:49:41 +02:00
commit bee600bfd8
80 changed files with 1373 additions and 400 deletions

View File

@ -1,6 +1,14 @@
name: OpenDTU Build name: OpenDTU Build
on: [push, pull_request] on:
push:
paths-ignore:
- docs/**
- '**/*.md'
pull_request:
paths-ignore:
- docs/**
- '**/*.md'
jobs: jobs:
get_default_envs: get_default_envs:
@ -92,12 +100,15 @@ jobs:
name: opendtu-${{ matrix.environment }} name: opendtu-${{ matrix.environment }}
path: | path: |
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin .pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
.pio/build/${{ matrix.environment }}/partitions.bin
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
name: opendtu-release 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: release:
name: Create Release name: Create Release

21
.github/workflows/cpplint.yml vendored Normal file
View 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

View File

@ -1,5 +1,8 @@
# OpenDTU_VeDirect # OpenDTU_VeDirect
[![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
[![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
## Background ## Background
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net). 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. 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 * Read live data from inverter
* Show inverters internal event log * Show inverters internal event log
* Show inverter information like firmware version, firmware build date, hardware revision and hardware version * 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+ * Uses ESP32 microcontroller and NRF24L01+
* Multi-Inverter support * Multi-Inverter support
* MQTT support (with TLS) * 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) * Build with [Vue.js](https://vuejs.org)
* Source is written in TypeScript * 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:
![NodeMCU-ESP32](docs/nodemcu-esp32.png)
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:
![nrf24l01plus](docs/nrf24l01plus.png)
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 ## Wiring up
### Schematic ### Schematic
![Schematic](docs/Wiring_ESP32_Schematic.png) ![Schematic](docs/Wiring_ESP32_Schematic.png)
@ -98,11 +135,13 @@ This can be achieved by editing the 'platformio.ini' file and add/change one or
## Flashing and starting up ## Flashing and starting up
### with Visual Studio Code ### 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) * 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) * Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/)
* 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) * 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.
* Adjust the COM port in the file "platformio.ini". It occurs twice: * 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 * upload_port
* monitor_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. * 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 * Building the microcontroller firmware
* Visual Studio Code with the PlatformIO Extension is required for building * 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
docs/nodemcu-esp32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
docs/nrf24l01plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -50,7 +50,7 @@ public:
IPAddress gatewayIP(); IPAddress gatewayIP();
IPAddress dnsIP(uint8_t dns_no = 0); IPAddress dnsIP(uint8_t dns_no = 0);
String macAddress(); String macAddress();
const char* getHostname(); static String getHostname();
bool isConnected(); bool isConnected();
network_mode NetworkMode(); network_mode NetworkMode();
@ -62,12 +62,13 @@ private:
void setStaticIp(); void setStaticIp();
void setupMode(); void setupMode();
void NetworkEvent(WiFiEvent_t event); void NetworkEvent(WiFiEvent_t event);
static uint32_t getChipId();
bool adminEnabled = true; bool adminEnabled = true;
bool forceDisconnection = false; bool forceDisconnection = false;
int adminTimeoutCounter = 0; int adminTimeoutCounter = 0;
int connectTimeoutTimer = 0; int connectTimeoutTimer = 0;
int connectRedoTimer = 0; int connectRedoTimer = 0;
unsigned long lastTimerCall = 0; uint32_t lastTimerCall = 0;
const byte DNS_PORT = 53; const byte DNS_PORT = 53;
IPAddress apIp; IPAddress apIp;
IPAddress apNetmask; IPAddress apNetmask;

View File

@ -1,11 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "WebApi_config.h"
#include "WebApi_devinfo.h" #include "WebApi_devinfo.h"
#include "WebApi_dtu.h" #include "WebApi_dtu.h"
#include "WebApi_eventlog.h" #include "WebApi_eventlog.h"
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "WebApi_inverter.h" #include "WebApi_inverter.h"
#include "WebApi_limit.h"
#include "WebApi_mqtt.h" #include "WebApi_mqtt.h"
#include "WebApi_network.h" #include "WebApi_network.h"
#include "WebApi_ntp.h" #include "WebApi_ntp.h"
@ -26,11 +28,13 @@ private:
AsyncWebServer _server; AsyncWebServer _server;
AsyncEventSource _events; AsyncEventSource _events;
WebApiConfigClass _webApiConfig;
WebApiDevInfoClass _webApiDevInfo; WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu; WebApiDtuClass _webApiDtu;
WebApiEventlogClass _webApiEventlog; WebApiEventlogClass _webApiEventlog;
WebApiFirmwareClass _webApiFirmware; WebApiFirmwareClass _webApiFirmware;
WebApiInverterClass _webApiInverter; WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit;
WebApiMqttClass _webApiMqtt; WebApiMqttClass _webApiMqtt;
WebApiNetworkClass _webApiNetwork; WebApiNetworkClass _webApiNetwork;
WebApiNtpClass _webApiNtp; WebApiNtpClass _webApiNtp;

18
include/WebApi_config.h Normal file
View 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
View 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;
};

View File

@ -14,7 +14,7 @@ private:
void onMqttStatus(AsyncWebServerRequest* request); void onMqttStatus(AsyncWebServerRequest* request);
void onMqttAdminGet(AsyncWebServerRequest* request); void onMqttAdminGet(AsyncWebServerRequest* request);
void onMqttAdminPost(AsyncWebServerRequest* request); void onMqttAdminPost(AsyncWebServerRequest* request);
String getRootCaCertInfo(char* cert); String getRootCaCertInfo(const char* cert);
AsyncWebServer* _server; AsyncWebServer* _server;
}; };

View File

@ -22,6 +22,6 @@ private:
uint32_t _lastWsPublish = 0; uint32_t _lastWsPublish = 0;
uint32_t _lastInvUpdateCheck = 0; uint32_t _lastInvUpdateCheck = 0;
unsigned long _lastWsCleanup = 0; uint32_t _lastWsCleanup = 0;
uint32_t _newestInverterTimestamp = 0; uint32_t _newestInverterTimestamp = 0;
}; };

View File

@ -31,6 +31,12 @@ void HoymilesClass::loop()
// Fetch event log // Fetch event log
iv->sendAlarmLogRequest(_radio.get()); 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) // Fetch dev info (but first fetch stats)
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSample() == 0)) { if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSample() == 0)) {
Serial.println(F("Request device info")); Serial.println(F("Request device info"));

View File

@ -7,6 +7,8 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#define HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL (10 * 60 * 1000) // 10 minutes
class HoymilesClass { class HoymilesClass {
public: public:
void init(); void init();
@ -28,7 +30,7 @@ private:
std::vector<std::shared_ptr<InverterAbstract>> _inverters; std::vector<std::shared_ptr<InverterAbstract>> _inverters;
std::unique_ptr<HoymilesRadio> _radio; std::unique_ptr<HoymilesRadio> _radio;
uint32_t _pollInterval; uint32_t _pollInterval = 0;
uint32_t _lastPoll = 0; uint32_t _lastPoll = 0;
}; };

View File

@ -9,8 +9,6 @@
#include <nRF24L01.h> #include <nRF24L01.h>
#include <queue> #include <queue>
using namespace std;
// number of fragments hold in buffer // number of fragments hold in buffer
#define FRAGMENT_BUFFER_SIZE 30 #define FRAGMENT_BUFFER_SIZE 30
@ -57,7 +55,7 @@ public:
template <typename T> template <typename T>
T* enqueCommand() T* enqueCommand()
{ {
_commandQueue.push(make_shared<T>()); _commandQueue.push(std::make_shared<T>());
return static_cast<T*>(_commandQueue.back().get()); return static_cast<T*>(_commandQueue.back().get());
} }
@ -75,12 +73,12 @@ private:
std::unique_ptr<SPIClass> _hspi; std::unique_ptr<SPIClass> _hspi;
std::unique_ptr<RF24> _radio; std::unique_ptr<RF24> _radio;
uint8_t _rxChLst[5] = { 3, 23, 40, 61, 75 }; 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 _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; CircularBuffer<fragment_t, FRAGMENT_BUFFER_SIZE> _rxBuffer;
TimeoutHelper _rxTimeout; TimeoutHelper _rxTimeout;
@ -89,5 +87,5 @@ private:
bool _busyFlag = false; bool _busyFlag = false;
queue<shared_ptr<CommandAbstract>> _commandQueue; std::queue<std::shared_ptr<CommandAbstract>> _commandQueue;
}; };

View File

@ -4,7 +4,7 @@
class AlarmDataCommand : public MultiDataCommand { class AlarmDataCommand : public MultiDataCommand {
public: 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); virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
}; };

View File

@ -10,7 +10,7 @@ class InverterAbstract;
class CommandAbstract { class CommandAbstract {
public: 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() {}; virtual ~CommandAbstract() {};
template <typename T> template <typename T>

View File

@ -4,5 +4,5 @@
class DevControlCommand : public CommandAbstract { class DevControlCommand : public CommandAbstract {
public: public:
DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0); explicit DevControlCommand(uint64_t target_address = 0, uint64_t router_address = 0);
}; };

View File

@ -4,7 +4,7 @@
class DevInfoAllCommand : public MultiDataCommand { class DevInfoAllCommand : public MultiDataCommand {
public: 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); virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
}; };

View File

@ -4,7 +4,7 @@
class DevInfoSampleCommand : public MultiDataCommand { class DevInfoSampleCommand : public MultiDataCommand {
public: 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); virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
}; };

View File

@ -6,7 +6,7 @@
class MultiDataCommand : public CommandAbstract { class MultiDataCommand : public CommandAbstract {
public: 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); void setTime(time_t time);
time_t getTime(); time_t getTime();

View File

@ -4,5 +4,5 @@
class ParaSetCommand : public CommandAbstract { class ParaSetCommand : public CommandAbstract {
public: public:
ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0); explicit ParaSetCommand(uint64_t target_address = 0, uint64_t router_address = 0);
}; };

View File

@ -0,0 +1,13 @@
# Class hierarchy
* CommandAbstract
* DevControlCommand
* MultiDataCommand
* AlarmDataCommand
* DevInfoAllCommand
* DevInfoSampleCommand
* RealTimeRunDataCommand
* SystemConfigParaCommand
* ParaSetCommand
* SingleDataCommand
* RequestFrameCommand

View File

@ -4,7 +4,7 @@
class RealTimeRunDataCommand : public MultiDataCommand { class RealTimeRunDataCommand : public MultiDataCommand {
public: 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); virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
}; };

View File

@ -4,7 +4,7 @@
class RequestFrameCommand : public SingleDataCommand { class RequestFrameCommand : public SingleDataCommand {
public: 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); void setFrameNo(uint8_t frame_no);
uint8_t getFrameNo(); uint8_t getFrameNo();

View File

@ -4,5 +4,5 @@
class SingleDataCommand : public CommandAbstract { class SingleDataCommand : public CommandAbstract {
public: public:
SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0); explicit SingleDataCommand(uint64_t target_address = 0, uint64_t router_address = 0);
}; };

View 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;
}

View 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);
};

View File

@ -1,6 +1,6 @@
#include "crc.h" #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; uint8_t crc = CRC8_INIT;
for (uint8_t i = 0; i < len; i++) { for (uint8_t i = 0; i < len; i++) {
@ -12,7 +12,7 @@ uint8_t crc8(uint8_t buf[], uint8_t len)
return crc; 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; uint16_t crc = start;
uint8_t shift = 0; uint8_t shift = 0;
@ -29,7 +29,7 @@ uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start)
return crc; 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; uint16_t crc = crcIn;
uint8_t idx, val = buf[(startBit >> 3)]; uint8_t idx, val = buf[(startBit >> 3)];

View File

@ -8,6 +8,6 @@
#define CRC16_MODBUS_POLYNOM 0xA001 #define CRC16_MODBUS_POLYNOM 0xA001
#define CRC16_NRF24_POLYNOM 0x1021 #define CRC16_NRF24_POLYNOM 0x1021
uint8_t crc8(uint8_t buf[], uint8_t len); uint8_t crc8(const uint8_t buf[], uint8_t len);
uint16_t crc16(uint8_t buf[], uint8_t len, uint16_t start = 0xffff); uint16_t crc16(const 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); uint16_t crc16nrf24(const uint8_t buf[], uint16_t lenBits, uint16_t startBit = 0, uint16_t crcIn = 0xffff);

View File

@ -4,7 +4,7 @@
class HM_1CH : public HM_Abstract { class HM_1CH : public HM_Abstract {
public: public:
HM_1CH(uint64_t serial); explicit HM_1CH(uint64_t serial);
static bool isValidSerial(uint64_t serial); static bool isValidSerial(uint64_t serial);
String typeName(); String typeName();
const byteAssign_t* getByteAssignment(); const byteAssign_t* getByteAssignment();

View File

@ -4,7 +4,7 @@
class HM_2CH : public HM_Abstract { class HM_2CH : public HM_Abstract {
public: public:
HM_2CH(uint64_t serial); explicit HM_2CH(uint64_t serial);
static bool isValidSerial(uint64_t serial); static bool isValidSerial(uint64_t serial);
String typeName(); String typeName();
const byteAssign_t* getByteAssignment(); const byteAssign_t* getByteAssignment();

View File

@ -4,7 +4,7 @@
class HM_4CH : public HM_Abstract { class HM_4CH : public HM_Abstract {
public: public:
HM_4CH(uint64_t serial); explicit HM_4CH(uint64_t serial);
static bool isValidSerial(uint64_t serial); static bool isValidSerial(uint64_t serial);
String typeName(); String typeName();
const byteAssign_t* getByteAssignment(); const byteAssign_t* getByteAssignment();

View File

@ -4,6 +4,7 @@
#include "commands/DevInfoAllCommand.h" #include "commands/DevInfoAllCommand.h"
#include "commands/DevInfoSampleCommand.h" #include "commands/DevInfoSampleCommand.h"
#include "commands/RealTimeRunDataCommand.h" #include "commands/RealTimeRunDataCommand.h"
#include "commands/SystemConfigParaCommand.h"
HM_Abstract::HM_Abstract(uint64_t serial) HM_Abstract::HM_Abstract(uint64_t serial)
: InverterAbstract(serial) {}; : InverterAbstract(serial) {};
@ -70,3 +71,20 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
return true; 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;
}

View File

@ -4,10 +4,11 @@
class HM_Abstract : public InverterAbstract { class HM_Abstract : public InverterAbstract {
public: public:
HM_Abstract(uint64_t serial); explicit HM_Abstract(uint64_t serial);
bool sendStatsRequest(HoymilesRadio* radio); bool sendStatsRequest(HoymilesRadio* radio);
bool sendAlarmLogRequest(HoymilesRadio* radio); bool sendAlarmLogRequest(HoymilesRadio* radio);
bool sendDevInfoRequest(HoymilesRadio* radio); bool sendDevInfoRequest(HoymilesRadio* radio);
bool sendSystemConfigParaRequest(HoymilesRadio* radio);
private: private:
uint8_t _lastAlarmLogCnt = 0; uint8_t _lastAlarmLogCnt = 0;

View File

@ -8,6 +8,7 @@ InverterAbstract::InverterAbstract(uint64_t serial)
_alarmLogParser.reset(new AlarmLogParser()); _alarmLogParser.reset(new AlarmLogParser());
_devInfoParser.reset(new DevInfoParser()); _devInfoParser.reset(new DevInfoParser());
_statisticsParser.reset(new StatisticsParser()); _statisticsParser.reset(new StatisticsParser());
_systemConfigParaParser.reset(new SystemConfigParaParser());
} }
void InverterAbstract::init() void InverterAbstract::init()
@ -54,6 +55,11 @@ StatisticsParser* InverterAbstract::Statistics()
return _statisticsParser.get(); return _statisticsParser.get();
} }
SystemConfigParaParser* InverterAbstract::SystemConfigPara()
{
return _systemConfigParaParser.get();
}
void InverterAbstract::clearRxFragmentBuffer() void InverterAbstract::clearRxFragmentBuffer()
{ {
memset(_rxFragmentBuffer, 0, MAX_RF_FRAGMENT_COUNT * sizeof(fragment_t)); memset(_rxFragmentBuffer, 0, MAX_RF_FRAGMENT_COUNT * sizeof(fragment_t));

View File

@ -1,8 +1,9 @@
#pragma once #pragma once
#include "../parser/AlarmLogParser.h" #include "../parser/AlarmLogParser.h"
#include "../parser/StatisticsParser.h"
#include "../parser/DevInfoParser.h" #include "../parser/DevInfoParser.h"
#include "../parser/StatisticsParser.h"
#include "../parser/SystemConfigParaParser.h"
#include "HoymilesRadio.h" #include "HoymilesRadio.h"
#include "types.h" #include "types.h"
#include <Arduino.h> #include <Arduino.h>
@ -24,7 +25,7 @@ class CommandAbstract;
class InverterAbstract { class InverterAbstract {
public: public:
InverterAbstract(uint64_t serial); explicit InverterAbstract(uint64_t serial);
void init(); void init();
uint64_t serial(); uint64_t serial();
void setName(const char* name); void setName(const char* name);
@ -40,14 +41,16 @@ public:
virtual bool sendStatsRequest(HoymilesRadio* radio) = 0; virtual bool sendStatsRequest(HoymilesRadio* radio) = 0;
virtual bool sendAlarmLogRequest(HoymilesRadio* radio) = 0; virtual bool sendAlarmLogRequest(HoymilesRadio* radio) = 0;
virtual bool sendDevInfoRequest(HoymilesRadio* radio) = 0; virtual bool sendDevInfoRequest(HoymilesRadio* radio) = 0;
virtual bool sendSystemConfigParaRequest(HoymilesRadio* radio) = 0;
AlarmLogParser* EventLog(); AlarmLogParser* EventLog();
DevInfoParser* DevInfo(); DevInfoParser* DevInfo();
StatisticsParser* Statistics(); StatisticsParser* Statistics();
SystemConfigParaParser* SystemConfigPara();
private: private:
serial_u _serial; serial_u _serial;
char _name[MAX_NAME_LENGTH]; char _name[MAX_NAME_LENGTH] = "";
fragment_t _rxFragmentBuffer[MAX_RF_FRAGMENT_COUNT]; fragment_t _rxFragmentBuffer[MAX_RF_FRAGMENT_COUNT];
uint8_t _rxFragmentMaxPacketId = 0; uint8_t _rxFragmentMaxPacketId = 0;
uint8_t _rxFragmentLastPacketId = 0; uint8_t _rxFragmentLastPacketId = 0;
@ -56,4 +59,5 @@ private:
std::unique_ptr<AlarmLogParser> _alarmLogParser; std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser; std::unique_ptr<DevInfoParser> _devInfoParser;
std::unique_ptr<StatisticsParser> _statisticsParser; std::unique_ptr<StatisticsParser> _statisticsParser;
std::unique_ptr<SystemConfigParaParser> _systemConfigParaParser;
}; };

View File

@ -272,5 +272,5 @@ int AlarmLogParser::getTimezoneOffset()
ptm->tm_isdst = -1; ptm->tm_isdst = -1;
gmt = mktime(ptm); gmt = mktime(ptm);
return (int)difftime(rawtime, gmt); return static_cast<int>(difftime(rawtime, gmt));
} }

View File

@ -98,7 +98,7 @@ uint16_t DevInfoParser::getHwVersion()
/* struct tm to seconds since Unix epoch */ /* struct tm to seconds since Unix epoch */
time_t DevInfoParser::timegm(struct tm* t) time_t DevInfoParser::timegm(struct tm* t)
{ {
register long year; register uint32_t year;
register time_t result; register time_t result;
#define MONTHSPERYEAR 12 /* months per calendar year */ #define MONTHSPERYEAR 12 /* months per calendar year */
static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; static const int cumdays[MONTHSPERYEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };

View File

@ -1,3 +1,4 @@
#pragma once
#include "Parser.h" #include "Parser.h"
#include <Arduino.h> #include <Arduino.h>
@ -30,9 +31,9 @@ private:
uint32_t _lastUpdateAll = 0; uint32_t _lastUpdateAll = 0;
uint32_t _lastUpdateSample = 0; uint32_t _lastUpdateSample = 0;
uint8_t _payloadDevInfoAll[DEV_INFO_SIZE]; uint8_t _payloadDevInfoAll[DEV_INFO_SIZE] = {};
uint8_t _devInfoAllLength; uint8_t _devInfoAllLength = 0;
uint8_t _payloadDevInfoSample[DEV_INFO_SIZE]; uint8_t _payloadDevInfoSample[DEV_INFO_SIZE] = {};
uint8_t _devInfoSampleLength; uint8_t _devInfoSampleLength = 0;
}; };

View File

@ -56,7 +56,7 @@ float StatisticsParser::getChannelFieldValue(uint8_t channel, uint8_t fieldId)
val |= _payloadStatistic[ptr]; val |= _payloadStatistic[ptr];
} while (++ptr != end); } while (++ptr != end);
return (float)(val) / (float)(div); return static_cast<float>(val) / static_cast<float>(div);
} else { } else {
// Value has to be calculated // Value has to be calculated
return calcFunctions[b[pos].start].func(this, b[pos].num); return calcFunctions[b[pos].start].func(this, b[pos].num);

View File

@ -114,8 +114,8 @@ public:
void setChannelMaxPower(uint8_t channel, uint16_t power); void setChannelMaxPower(uint8_t channel, uint16_t power);
private: private:
uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE]; uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {};
uint8_t _statisticLength; uint8_t _statisticLength = 0;
uint16_t _chanMaxPower[CH4]; uint16_t _chanMaxPower[CH4];
const byteAssign_t* _byteAssignment; const byteAssign_t* _byteAssignment;

View 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;
}

View 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;
};

View File

@ -14,34 +14,38 @@ default_envs = generic
[env] [env]
framework = arduino framework = arduino
platform = espressif32@>=5 platform = espressif32@>=5
build_flags = build_flags =
-D=${PIOENV} -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 -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 = lib_deps =
https://github.com/me-no-dev/ESPAsyncWebServer.git https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.19.4 bblanchon/ArduinoJson @ ^6.19.4
https://github.com/bertmelis/espMqttClient.git https://github.com/bertmelis/espMqttClient.git#v1.2.3
nrf24/RF24 @ ^1.4.5 nrf24/RF24 @ ^1.4.5
extra_scripts = pre:auto_firmware_version.py
extra_scripts =
pre:auto_firmware_version.py
board_build.partitions = partitions_custom.csv board_build.partitions = partitions_custom.csv
board_build.filesystem = littlefs board_build.filesystem = littlefs
monitor_filters = monitor_filters = time, colorize, log2file, esp32_exception_decoder
time
colorize
log2file
esp32_exception_decoder
monitor_speed = 115200 monitor_speed = 115200
upload_protocol = esptool upload_protocol = esptool
[env:generic] [env:generic]
board = esp32dev board = esp32dev
monitor_port = COM4 monitor_port = COM4
upload_port = COM4 upload_port = COM4
[env:olimex_esp32_poe] [env:olimex_esp32_poe]
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
board = esp32-poe board = esp32-poe
build_flags = build_flags = ${env.build_flags}
${env.build_flags}
-DHOYMILES_PIN_MISO=15 -DHOYMILES_PIN_MISO=15
-DHOYMILES_PIN_MOSI=2 -DHOYMILES_PIN_MOSI=2
-DHOYMILES_PIN_SCLK=14 -DHOYMILES_PIN_SCLK=14
@ -49,9 +53,28 @@ build_flags =
-DHOYMILES_PIN_CE=16 -DHOYMILES_PIN_CE=16
-DHOYMILES_PIN_CS=5 -DHOYMILES_PIN_CS=5
-DOPENDTU_ETHERNET -DOPENDTU_ETHERNET
monitor_port = COM3 monitor_port = COM3
upload_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] [env:d1 mini esp32]
board = wemos_d1_mini32 board = wemos_d1_mini32
build_flags = build_flags =

View File

@ -70,7 +70,7 @@ bool ConfigurationClass::write()
return false; return false;
} }
config.Cfg_SaveCount++; 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++) { for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
f.write(bytes[i]); f.write(bytes[i]);
} }
@ -84,7 +84,7 @@ bool ConfigurationClass::read()
if (!f) { if (!f) {
return false; 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++) { for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) {
bytes[i] = f.read(); bytes[i] = f.read();
} }

View File

@ -46,7 +46,7 @@ void MqttHassPublishingClass::publishConfig()
return; return;
} }
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// Loop all inverters // Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { 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]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

@ -19,18 +19,21 @@ void MqttPublishingClass::loop()
return; return;
} }
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) {
MqttSettings.publish("dtu/uptime", String(millis() / 1000)); MqttSettings.publish("dtu/uptime", String(millis() / 1000));
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
MqttSettings.publish("dtu/rssi", String(WiFi.RSSI()));
}
// Loop all inverters // Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
char buffer[sizeof(uint64_t) * 8 + 1]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));
String subtopic = String(buffer); String subtopic = String(buffer);
@ -58,6 +61,11 @@ void MqttPublishingClass::loop()
MqttSettings.publish(subtopic + "/device/hwversion", String(inv->DevInfo()->getHwVersion())); 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(); uint32_t lastUpdate = inv->Statistics()->getLastUpdate();
if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) {
_lastPublishStats[i] = lastUpdate; _lastPublishStats[i] = lastUpdate;
@ -94,7 +102,7 @@ String MqttPublishingClass::getTopic(std::shared_ptr<InverterAbstract> inv, uint
} }
char buffer[sizeof(uint64_t) * 8 + 1]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));
String invSerial = String(buffer); String invSerial = String(buffer);

View File

@ -30,7 +30,7 @@ void MqttSettingsClass::NetworkEvent(network_event event)
void MqttSettingsClass::onMqttConnect(bool sessionPresent) void MqttSettingsClass::onMqttConnect(bool sessionPresent)
{ {
Serial.println(F("Connected to MQTT.")); 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); publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online);
} }
@ -68,9 +68,9 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
void MqttSettingsClass::performConnect() void MqttSettingsClass::performConnect()
{ {
if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) { if (NetworkSettings.isConnected() && Configuration.get().Mqtt_Enabled) {
using namespace std::placeholders; using std::placeholders::_1;
Serial.println(F("Connecting to MQTT...")); Serial.println(F("Connecting to MQTT..."));
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
willTopic = getPrefix() + config.Mqtt_LwtTopic; willTopic = getPrefix() + config.Mqtt_LwtTopic;
clientId = NetworkSettings.getApName(); clientId = NetworkSettings.getApName();
if (config.Mqtt_Tls) { if (config.Mqtt_Tls) {
@ -95,7 +95,7 @@ void MqttSettingsClass::performConnect()
void MqttSettingsClass::performDisconnect() void MqttSettingsClass::performDisconnect()
{ {
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Offline); publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Offline);
mqttClient->disconnect(); mqttClient->disconnect();
} }
@ -136,7 +136,7 @@ void MqttSettingsClass::publishHass(String subtopic, String payload)
void MqttSettingsClass::init() void MqttSettingsClass::init()
{ {
using namespace std::placeholders; using std::placeholders::_1;
NetworkSettings.onEvent(std::bind(&MqttSettingsClass::NetworkEvent, this, _1)); NetworkSettings.onEvent(std::bind(&MqttSettingsClass::NetworkEvent, this, _1));
createMqttClientObject(); createMqttClientObject();
@ -146,7 +146,7 @@ void MqttSettingsClass::createMqttClientObject()
{ {
if (mqttClient != nullptr) if (mqttClient != nullptr)
delete mqttClient; delete mqttClient;
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
if (config.Mqtt_Tls) { if (config.Mqtt_Tls) {
mqttClient = static_cast<MqttClient*>(new espMqttClientSecure); mqttClient = static_cast<MqttClient*>(new espMqttClientSecure);
} else { } else {

View File

@ -19,7 +19,7 @@ NetworkSettingsClass::NetworkSettingsClass()
void NetworkSettingsClass::init() void NetworkSettingsClass::init()
{ {
using namespace std::placeholders; using std::placeholders::_1;
WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1));
setupMode(); setupMode();
@ -142,11 +142,7 @@ void NetworkSettingsClass::enableAdminMode()
String NetworkSettingsClass::getApName() String NetworkSettingsClass::getApName()
{ {
uint32_t chipId = 0; return String(ACCESS_POINT_NAME + String(getChipId()));
for (int i = 0; i < 17; i += 8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
return String(ACCESS_POINT_NAME + String(chipId));
} }
void NetworkSettingsClass::loop() void NetworkSettingsClass::loop()
@ -241,32 +237,37 @@ void NetworkSettingsClass::applyConfig()
void NetworkSettingsClass::setHostname() void NetworkSettingsClass::setHostname()
{ {
Serial.print(F("Setting Hostname... ")); Serial.print(F("Setting Hostname... "));
if (strcmp(Configuration.get().WiFi_Hostname, "")) {
if (_networkMode == network_mode::WiFi) { if (_networkMode == network_mode::WiFi) {
if (WiFi.hostname(Configuration.get().WiFi_Hostname)) { if (WiFi.hostname(getHostname())) {
Serial.println(F("done")); Serial.println(F("done"));
} else { } else {
Serial.println(F("failed")); Serial.println(F("failed"));
} }
// Evil bad hack to get the hostname set up correctly
WiFi.mode(WIFI_MODE_APSTA);
WiFi.mode(WIFI_MODE_STA);
setupMode();
} }
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
else if (_networkMode == network_mode::Ethernet) { else if (_networkMode == network_mode::Ethernet) {
if (ETH.setHostname(Configuration.get().WiFi_Hostname)) { if (ETH.setHostname(getHostname().c_str())) {
Serial.println(F("done")); Serial.println(F("done"));
} else { } else {
Serial.println(F("failed")); Serial.println(F("failed"));
} }
} }
#endif #endif
} else {
Serial.println(F("failed (Hostname empty)"));
}
} }
void NetworkSettingsClass::setStaticIp() void NetworkSettingsClass::setStaticIp()
{ {
if (_networkMode == network_mode::WiFi) { 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... ")); Serial.print(F("Configuring WiFi STA static IP... "));
WiFi.config( WiFi.config(
IPAddress(Configuration.get().WiFi_Ip), IPAddress(Configuration.get().WiFi_Ip),
@ -280,7 +281,9 @@ void NetworkSettingsClass::setStaticIp()
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
else if (_networkMode == network_mode::Ethernet) { else if (_networkMode == network_mode::Ethernet) {
if (Configuration.get().WiFi_Dhcp) { if (Configuration.get().WiFi_Dhcp) {
Serial.print(F("Configuring Ethernet DHCP IP... "));
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
Serial.println(F("done"));
} else { } else {
Serial.print(F("Configuring Ethernet static IP... ")); Serial.print(F("Configuring Ethernet static IP... "));
ETH.config( ETH.config(
@ -375,14 +378,43 @@ String NetworkSettingsClass::macAddress()
} }
} }
const char* NetworkSettingsClass::getHostname() String NetworkSettingsClass::getHostname()
{ {
#ifdef OPENDTU_ETHERNET const CONFIG_T& config = Configuration.get();
if (_networkMode == network_mode::Ethernet) { char preparedHostname[WIFI_MAX_HOSTNAME_STRLEN + 1];
return ETH.getHostname(); 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++;
} }
#endif // else do nothing - no leading hyphens and do not include hyphens for all other characters.
return WiFi.getHostname(); pC++;
}
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() bool NetworkSettingsClass::isConnected()
@ -399,4 +431,13 @@ network_mode NetworkSettingsClass::NetworkMode()
return _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; NetworkSettingsClass NetworkSettings;

View File

@ -15,15 +15,15 @@ WebApiClass::WebApiClass()
void WebApiClass::init() void WebApiClass::init()
{ {
using namespace std::placeholders;
_server.addHandler(&_events); _server.addHandler(&_events);
_webApiConfig.init(&_server);
_webApiDevInfo.init(&_server); _webApiDevInfo.init(&_server);
_webApiDtu.init(&_server); _webApiDtu.init(&_server);
_webApiEventlog.init(&_server); _webApiEventlog.init(&_server);
_webApiFirmware.init(&_server); _webApiFirmware.init(&_server);
_webApiInverter.init(&_server); _webApiInverter.init(&_server);
_webApiLimit.init(&_server);
_webApiMqtt.init(&_server); _webApiMqtt.init(&_server);
_webApiNetwork.init(&_server); _webApiNetwork.init(&_server);
_webApiNtp.init(&_server); _webApiNtp.init(&_server);
@ -38,11 +38,13 @@ void WebApiClass::init()
void WebApiClass::loop() void WebApiClass::loop()
{ {
_webApiConfig.loop();
_webApiDevInfo.loop(); _webApiDevInfo.loop();
_webApiDtu.loop(); _webApiDtu.loop();
_webApiEventlog.loop(); _webApiEventlog.loop();
_webApiFirmware.loop(); _webApiFirmware.loop();
_webApiInverter.loop(); _webApiInverter.loop();
_webApiLimit.loop();
_webApiMqtt.loop(); _webApiMqtt.loop();
_webApiNetwork.loop(); _webApiNetwork.loop();
_webApiNtp.loop(); _webApiNtp.loop();

125
src/WebApi_config.cpp Normal file
View 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();
}
}

View File

@ -10,7 +10,7 @@
void WebApiDevInfoClass::init(AsyncWebServer* server) void WebApiDevInfoClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -31,7 +31,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
// Inverter Serial is read as HEX // Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

@ -10,7 +10,7 @@
void WebApiDtuClass::init(AsyncWebServer* server) void WebApiDtuClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -26,11 +26,11 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// DTU Serial is read as HEX // DTU Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; 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 >> 32) & 0xFFFFFFFF)),
((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF)));
root[F("dtu_serial")] = buffer; root[F("dtu_serial")] = buffer;
@ -102,9 +102,9 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
char* t;
// Interpret the string as a hex value and convert it to uint64_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_PollInterval = root[F("dtu_pollinterval")].as<uint32_t>();
config.Dtu_PaLevel = root[F("dtu_palevel")].as<uint8_t>(); config.Dtu_PaLevel = root[F("dtu_palevel")].as<uint8_t>();
Configuration.write(); Configuration.write();

View File

@ -9,7 +9,7 @@
void WebApiEventlogClass::init(AsyncWebServer* server) void WebApiEventlogClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -22,15 +22,21 @@ void WebApiEventlogClass::loop()
void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { uint64_t serial = 0;
auto inv = Hoymiles.getInverterByPos(i); 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 // Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

@ -11,7 +11,12 @@
void WebApiFirmwareClass::init(AsyncWebServer* server) 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; _server = server;

View File

@ -12,7 +12,7 @@
void WebApiInverterClass::init(AsyncWebServer* server) void WebApiInverterClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -32,7 +32,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
JsonArray data = root.createNestedArray(F("inverter")); 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++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) { if (config.Inverter[i].Serial > 0) {
@ -42,7 +42,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
// Inverter Serial is read as HEX // Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; 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 >> 32) & 0xFFFFFFFF)),
((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF)));
obj[F("serial")] = buffer; obj[F("serial")] = buffer;
@ -126,9 +126,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
return; return;
} }
char* t;
// Interpret the string as a hex value and convert it to uint64_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); strncpy(inverter->Name, root[F("name")].as<String>().c_str(), INV_MAX_NAME_STRLEN);
Configuration.write(); Configuration.write();
@ -224,8 +223,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as<uint8_t>()]; 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(), NULL, 16);
uint64_t new_serial = strtoll(root[F("serial")].as<String>().c_str(), &t, 16);
uint64_t old_serial = inverter.Serial; uint64_t old_serial = inverter.Serial;
// Interpret the string as a hex value and convert it to uint64_t // Interpret the string as a hex value and convert it to uint64_t

42
src/WebApi_limit.cpp Normal file
View 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);
}

View File

@ -12,7 +12,7 @@
void WebApiMqttClass::init(AsyncWebServer* server) void WebApiMqttClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -29,7 +29,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("mqtt_enabled")] = config.Mqtt_Enabled; root[F("mqtt_enabled")] = config.Mqtt_Enabled;
root[F("mqtt_hostname")] = config.Mqtt_Hostname; 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); AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot(); 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_enabled")] = config.Mqtt_Enabled;
root[F("mqtt_hostname")] = config.Mqtt_Hostname; 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_Enabled = root[F("mqtt_enabled")].as<bool>();
config.Mqtt_Retain = root[F("mqtt_retain")].as<bool>(); config.Mqtt_Retain = root[F("mqtt_retain")].as<bool>();
config.Mqtt_Tls = root[F("mqtt_tls")].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>(); config.Mqtt_Port = root[F("mqtt_port")].as<uint>();
strcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str()); strlcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str(), sizeof(config.Mqtt_Hostname));
strcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str()); strlcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str(), sizeof(config.Mqtt_Username));
strcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str()); strlcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str(), sizeof(config.Mqtt_Password));
strcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str()); strlcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Mqtt_Topic));
strcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str()); strlcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str(), sizeof(config.Mqtt_LwtTopic));
strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str()); strlcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Online));
strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str()); 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_PublishInterval = root[F("mqtt_publish_interval")].as<uint32_t>();
config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as<bool>(); 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_Expire = root[F("mqtt_hass_expire")].as<bool>();
config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>(); config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>();
config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].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(); Configuration.write();
retMsg[F("type")] = F("success"); retMsg[F("type")] = F("success");
@ -267,18 +267,18 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
MqttHassPublishing.forceUpdate(); MqttHassPublishing.forceUpdate();
} }
String WebApiMqttClass::getRootCaCertInfo(char* cert) String WebApiMqttClass::getRootCaCertInfo(const char* cert)
{ {
char rootCaCertInfo[1024] = ""; char rootCaCertInfo[1024] = "";
mbedtls_x509_crt global_cacert; 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); mbedtls_x509_crt_init(&global_cacert);
int ret = mbedtls_x509_crt_parse(&global_cacert, const_cast<unsigned char*>((unsigned char*)cert), 1 + strlen(cert)); int ret = mbedtls_x509_crt_parse(&global_cacert, const_cast<unsigned char*>((unsigned char*)cert), 1 + strlen(cert));
if (ret < 0) { 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); mbedtls_x509_crt_free(&global_cacert);
return ""; return "";
} }

View File

@ -11,7 +11,7 @@
void WebApiNetworkClass::init(AsyncWebServer* server) void WebApiNetworkClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -32,6 +32,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0); root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0);
root[F("sta_ssid")] = WiFi.SSID(); root[F("sta_ssid")] = WiFi.SSID();
root[F("sta_rssi")] = WiFi.RSSI(); root[F("sta_rssi")] = WiFi.RSSI();
root[F("network_hostname")] = NetworkSettings.getHostname();
root[F("network_ip")] = NetworkSettings.localIP().toString(); root[F("network_ip")] = NetworkSettings.localIP().toString();
root[F("network_netmask")] = NetworkSettings.subnetMask().toString(); root[F("network_netmask")] = NetworkSettings.subnetMask().toString();
root[F("network_gateway")] = NetworkSettings.gatewayIP().toString(); root[F("network_gateway")] = NetworkSettings.gatewayIP().toString();
@ -53,7 +54,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
root[F("hostname")] = config.WiFi_Hostname; root[F("hostname")] = config.WiFi_Hostname;
root[F("dhcp")] = config.WiFi_Dhcp; root[F("dhcp")] = config.WiFi_Dhcp;
@ -184,9 +185,9 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
config.WiFi_Dns2[1] = dns2[1]; config.WiFi_Dns2[1] = dns2[1];
config.WiFi_Dns2[2] = dns2[2]; config.WiFi_Dns2[2] = dns2[2];
config.WiFi_Dns2[3] = dns2[3]; config.WiFi_Dns2[3] = dns2[3];
strcpy(config.WiFi_Ssid, root[F("ssid")].as<String>().c_str()); strlcpy(config.WiFi_Ssid, root[F("ssid")].as<String>().c_str(), sizeof(config.WiFi_Ssid));
strcpy(config.WiFi_Password, root[F("password")].as<String>().c_str()); strlcpy(config.WiFi_Password, root[F("password")].as<String>().c_str(), sizeof(config.WiFi_Password));
strcpy(config.WiFi_Hostname, root[F("hostname")].as<String>().c_str()); strlcpy(config.WiFi_Hostname, root[F("hostname")].as<String>().c_str(), sizeof(config.WiFi_Hostname));
if (root[F("dhcp")].as<bool>()) { if (root[F("dhcp")].as<bool>()) {
config.WiFi_Dhcp = true; config.WiFi_Dhcp = true;
} else { } else {

View File

@ -11,7 +11,7 @@
void WebApiNtpClass::init(AsyncWebServer* server) void WebApiNtpClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -28,7 +28,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); 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_server")] = config.Ntp_Server;
root[F("ntp_timezone")] = config.Ntp_Timezone; root[F("ntp_timezone")] = config.Ntp_Timezone;
@ -52,7 +52,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
{ {
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot(); 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_server")] = config.Ntp_Server;
root[F("ntp_timezone")] = config.Ntp_Timezone; root[F("ntp_timezone")] = config.Ntp_Timezone;
@ -123,9 +123,9 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
} }
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
strcpy(config.Ntp_Server, root[F("ntp_server")].as<String>().c_str()); strlcpy(config.Ntp_Server, root[F("ntp_server")].as<String>().c_str(), sizeof(config.Ntp_Server));
strcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as<String>().c_str()); strlcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as<String>().c_str(), sizeof(config.Ntp_Timezone));
strcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as<String>().c_str()); strlcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as<String>().c_str(), sizeof(config.Ntp_TimezoneDescr));
Configuration.write(); Configuration.write();
retMsg[F("type")] = F("success"); retMsg[F("type")] = F("success");

View File

@ -16,7 +16,7 @@
void WebApiSysstatusClass::init(AsyncWebServer* server) void WebApiSysstatusClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders; using std::placeholders::_1;
_server = server; _server = server;
@ -58,7 +58,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount; root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount;
char version[16]; 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("firmware_version")] = version;
root[F("git_hash")] = AUTO_GIT_HASH; root[F("git_hash")] = AUTO_GIT_HASH;

View File

@ -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) void WebApiWebappClass::init(AsyncWebServer* server)
{ {
using namespace std::placeholders;
_server = server; _server = server;
_server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { _server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {

View File

@ -13,7 +13,12 @@ WebApiWsLiveClass::WebApiWsLiveClass()
void WebApiWsLiveClass::init(AsyncWebServer* server) 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 = server;
_server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); _server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1));
@ -56,10 +61,9 @@ void WebApiWsLiveClass::loop()
JsonVariant var = root; JsonVariant var = root;
generateJsonResponse(var); generateJsonResponse(var);
size_t len = measureJson(root); String buffer;
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); // creates a buffer (len + 1) for you.
if (buffer) { if (buffer) {
serializeJson(root, (char*)buffer->get(), len + 1); serializeJson(root, buffer);
_ws.textAll(buffer); _ws.textAll(buffer);
} }
@ -74,7 +78,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
char buffer[sizeof(uint64_t) * 8 + 1]; 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() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF))); ((uint32_t)(inv->serial() & 0xFFFFFFFF)));
@ -135,11 +139,11 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC
{ {
if (type == WS_EVT_CONNECT) { if (type == WS_EVT_CONNECT) {
char str[64]; 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); Serial.println(str);
} else if (type == WS_EVT_DISCONNECT) { } else if (type == WS_EVT_DISCONNECT) {
char str[64]; 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); Serial.println(str);
} }
} }

View File

@ -81,7 +81,7 @@ void setup()
// Initialize inverter communication // Initialize inverter communication
Serial.print(F("Initialize Hoymiles interface... ")); Serial.print(F("Initialize Hoymiles interface... "));
CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
Hoymiles.init(); Hoymiles.init();
Serial.println(F(" Setting radio PA level... ")); Serial.println(F(" Setting radio PA level... "));

View File

@ -9,30 +9,30 @@
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.0", "bootstrap": "^5.2.1",
"bootstrap-icons-vue": "^1.8.1", "bootstrap-icons-vue": "^1.8.1",
"core-js": "^3.25.0", "core-js": "^3.25.1",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.2.38", "vue": "^3.2.39",
"vue-class-component": "^8.0.0-0", "vue-class-component": "^8.0.0-0",
"vue-router": "^4.1.5" "vue-router": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.13", "@babel/core": "^7.19.0",
"@babel/eslint-parser": "^7.18.9", "@babel/eslint-parser": "^7.18.9",
"@types/bootstrap": "^5.2.2", "@types/bootstrap": "^5.2.4",
"@types/node": "^18.7.15", "@types/node": "^18.7.16",
"@types/spark-md5": "^3.0.2", "@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-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "^5.0.6", "@vue/cli-plugin-router": "^5.0.6",
"@vue/cli-plugin-typescript": "^5.0.8", "@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.1",
"eslint": "^8.23.0", "eslint": "^8.23.1",
"eslint-plugin-vue": "^9.4.0", "eslint-plugin-vue": "^9.4.0",
"typescript": "^4.8.2", "typescript": "^4.8.3",
"vue-cli-plugin-compression": "~2.0.0" "vue-cli-plugin-compression": "~2.0.0"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -1,10 +1,101 @@
<template> <template>
<div class="container-xxl" role="main"> <div class="container-xxl" role="main">
<div class="page-header"> <div class="page-header">
<h1>About</h1> <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>&nbsp;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 This project was started from
<a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion. <a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion.
(Mikrocontroller.net)</a> (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>
</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>&nbsp;News &amp; 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>&nbsp;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>&nbsp;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> </template>

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

View File

@ -39,6 +39,14 @@
{{ inverter.data_age }} seconds) {{ inverter.data_age }} seconds)
<div class="btn-toolbar" role="toolbar"> <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"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-info" <button type="button" class="btn btn-sm btn-info"
@click="onShowDevInfo(inverter.serial)" title="Show Inverter Info"> @click="onShowDevInfo(inverter.serial)" title="Show Inverter Info">
@ -130,6 +138,30 @@
</div> </div>
</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> </div>
</template> </template>
@ -139,6 +171,7 @@ import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
import EventLog from '@/components/partials/EventLog.vue'; import EventLog from '@/components/partials/EventLog.vue';
import DevInfo from '@/components/partials/DevInfo.vue'; import DevInfo from '@/components/partials/DevInfo.vue';
import LimitSettingsCurrent from '@/components/partials/LimitSettingsCurrent.vue';
import VedirectView from '@/components/partials/VedirectView.vue'; import VedirectView from '@/components/partials/VedirectView.vue';
declare interface Inverter { declare interface Inverter {
@ -154,6 +187,7 @@ export default defineComponent({
InverterChannelInfo, InverterChannelInfo,
EventLog, EventLog,
DevInfo, DevInfo,
LimitSettingsCurrent,
VedirectView VedirectView
}, },
data() { data() {
@ -169,7 +203,10 @@ export default defineComponent({
eventLogLoading: true, eventLogLoading: true,
devInfoView: {} as bootstrap.Modal, devInfoView: {} as bootstrap.Modal,
devInfoList: {}, devInfoList: {},
devInfoLoading: true devInfoLoading: true,
limitSettingView: {} as bootstrap.Modal,
limitSettingList: {},
limitSettingLoading: true,
}; };
}, },
created() { created() {
@ -180,6 +217,7 @@ export default defineComponent({
mounted() { mounted() {
this.eventLogView = new bootstrap.Modal('#eventView'); this.eventLogView = new bootstrap.Modal('#eventView');
this.devInfoView = new bootstrap.Modal('#devInfoView'); this.devInfoView = new bootstrap.Modal('#devInfoView');
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
}, },
unmounted() { unmounted() {
this.closeSocket(); this.closeSocket();
@ -264,7 +302,7 @@ export default defineComponent({
}, },
onShowEventlog(serial: number) { onShowEventlog(serial: number) {
this.eventLogLoading = true; this.eventLogLoading = true;
fetch("/api/eventlog/status") fetch("/api/eventlog/status?inv=" + serial)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
this.eventLogList = data[serial]; this.eventLogList = data[serial];
@ -287,6 +325,20 @@ export default defineComponent({
this.devInfoView.show(); 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> </script>

View File

@ -39,6 +39,9 @@
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li>
<router-link @click="onClick" class="dropdown-item" to="/settings/config">Config Management</router-link>
</li>
<li> <li>
<router-link @click="onClick" class="dropdown-item" to="/firmware/upgrade">Firmware Upgrade</router-link> <router-link @click="onClick" class="dropdown-item" to="/firmware/upgrade">Firmware Upgrade</router-link>
</li> </li>

View File

@ -39,6 +39,11 @@
<div class="col-sm-10"> <div class="col-sm-10">
<input type="text" class="form-control" id="inputHostname" maxlength="32" <input type="text" class="form-control" id="inputHostname" maxlength="32"
placeholder="Hostname" v-model="networkConfigList.hostname" /> 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>
</div> </div>

View File

@ -49,6 +49,7 @@ export default defineComponent({
ap_ssid: "", ap_ssid: "",
ap_stationnum: 0, ap_stationnum: 0,
// InterfaceNetworkInfo // InterfaceNetworkInfo
network_hostname: "",
network_ip: "", network_ip: "",
network_netmask: "", network_netmask: "",
network_gateway: "", network_gateway: "",

View File

@ -7,6 +7,10 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>
<tr>
<th>Hostname</th>
<td>{{ network_hostname }}</td>
</tr>
<tr> <tr>
<th>IP Address</th> <th>IP Address</th>
<td>{{ network_ip }}</td> <td>{{ network_ip }}</td>
@ -43,6 +47,7 @@ import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
network_hostname: String,
network_ip: String, network_ip: String,
network_netmask: String, network_netmask: String,
network_gateway: String, network_gateway: String,

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

View File

@ -11,6 +11,7 @@ import MqttInfoView from '@/components/MqttInfoView.vue'
import InverterAdminView from '@/components/InverterAdminView.vue' import InverterAdminView from '@/components/InverterAdminView.vue'
import DtuAdminView from '@/components/DtuAdminView.vue' import DtuAdminView from '@/components/DtuAdminView.vue'
import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue' import FirmwareUpgradeView from '@/components/FirmwareUpgradeView.vue'
import ConfigAdminView from '@/components/ConfigAdminView.vue'
import VedirectAdminView from '@/components/VedirectAdminView.vue' import VedirectAdminView from '@/components/VedirectAdminView.vue'
import VedirectInfoView from '@/components/VedirectInfoView.vue' import VedirectInfoView from '@/components/VedirectInfoView.vue'
@ -84,6 +85,11 @@ const routes: Array<RouteRecordRaw> = [
path: '/firmware/upgrade', path: '/firmware/upgrade',
name: 'Firmware Upgrade', name: 'Firmware Upgrade',
component: FirmwareUpgradeView component: FirmwareUpgradeView
},
{
path: '/settings/config',
name: 'Config Management',
component: ConfigAdminView
} }
]; ];

View File

@ -36,6 +36,11 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== 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": "@babel/core@^7.12.16":
version "7.18.10" version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8"
@ -57,21 +62,21 @@
json5 "^2.2.1" json5 "^2.2.1"
semver "^6.3.0" semver "^6.3.0"
"@babel/core@^7.18.13": "@babel/core@^7.19.0":
version "7.18.13" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.13.tgz#9be8c44512751b05094a4d3ab05fc53a47ce00ac" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.0.tgz#d2f5f4f2033c00de8096be3c9f45772563e150c3"
integrity sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A== integrity sha512-reM4+U7B9ss148rh2n1Qs9ASS+w94irYXga7c2jaQv9RVzpS7Mv1a9rnYYwuDa45G+DkORt9g6An2k/V4d9LbQ==
dependencies: dependencies:
"@ampproject/remapping" "^2.1.0" "@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.18.6" "@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.18.13" "@babel/generator" "^7.19.0"
"@babel/helper-compilation-targets" "^7.18.9" "@babel/helper-compilation-targets" "^7.19.0"
"@babel/helper-module-transforms" "^7.18.9" "@babel/helper-module-transforms" "^7.19.0"
"@babel/helpers" "^7.18.9" "@babel/helpers" "^7.19.0"
"@babel/parser" "^7.18.13" "@babel/parser" "^7.19.0"
"@babel/template" "^7.18.10" "@babel/template" "^7.18.10"
"@babel/traverse" "^7.18.13" "@babel/traverse" "^7.19.0"
"@babel/types" "^7.18.13" "@babel/types" "^7.19.0"
convert-source-map "^1.7.0" convert-source-map "^1.7.0"
debug "^4.1.0" debug "^4.1.0"
gensync "^1.0.0-beta.2" gensync "^1.0.0-beta.2"
@ -96,12 +101,12 @@
"@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1" jsesc "^2.5.1"
"@babel/generator@^7.18.13": "@babel/generator@^7.19.0":
version "7.18.13" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.13.tgz#59550cbb9ae79b8def15587bdfbaa388c4abf212" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.0.tgz#785596c06425e59334df2ccee63ab166b738419a"
integrity sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ== integrity sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg==
dependencies: dependencies:
"@babel/types" "^7.18.13" "@babel/types" "^7.19.0"
"@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1" jsesc "^2.5.1"
@ -130,6 +135,16 @@
browserslist "^4.20.2" browserslist "^4.20.2"
semver "^6.3.0" 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": "@babel/helper-create-class-features-plugin@^7.17.12", "@babel/helper-create-class-features-plugin@^7.18.0":
version "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" 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/template" "^7.18.6"
"@babel/types" "^7.18.9" "@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": "@babel/helper-hoist-variables@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" 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/traverse" "^7.18.9"
"@babel/types" "^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": "@babel/helper-optimise-call-expression@^7.16.7":
version "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" 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/traverse" "^7.18.9"
"@babel/types" "^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": "@babel/highlight@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" 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" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9"
integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==
"@babel/parser@^7.18.13": "@babel/parser@^7.19.0":
version "7.18.13" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.0.tgz#497fcafb1d5b61376959c1c338745ef0577aa02c"
integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg== integrity sha512-74bEXKX2h+8rrfQUfsBfuZZHzsEs6Eql4pqy/T4Nn6Y9wNPggQOqD6z6pn5Bl8ZfysKouFZT/UXEH94ummEeQw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.17.12": "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.17.12":
version "7.17.12" version "7.17.12"
@ -1040,19 +1086,19 @@
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
"@babel/traverse@^7.18.13": "@babel/traverse@^7.19.0":
version "7.18.13" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.13.tgz#5ab59ef51a997b3f10c4587d648b9696b6cb1a68" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.0.tgz#eb9c561c7360005c592cc645abafe0c3c4548eed"
integrity sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA== integrity sha512-4pKpFRDh+utd2mbRC8JLnlsMUii3PMHjpL6a0SZ4NMZy7YFP9aXORxEhdMVOc9CpWtDF09IkciQLEhK7Ml7gRA==
dependencies: dependencies:
"@babel/code-frame" "^7.18.6" "@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-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-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.18.13" "@babel/parser" "^7.19.0"
"@babel/types" "^7.18.13" "@babel/types" "^7.19.0"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
@ -1065,19 +1111,19 @@
"@babel/helper-validator-identifier" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.18.13": "@babel/types@^7.19.0":
version "7.18.13" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.0.tgz#75f21d73d73dc0351f3368d28db73465f4814600"
integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ== integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==
dependencies: dependencies:
"@babel/helper-string-parser" "^7.18.10" "@babel/helper-string-parser" "^7.18.10"
"@babel/helper-validator-identifier" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@eslint/eslintrc@^1.3.1": "@eslint/eslintrc@^1.3.2":
version "1.3.1" version "1.3.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.1.tgz#de0807bfeffc37b964a7d0400e0c348ce5a2543d" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356"
integrity sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ== integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==
dependencies: dependencies:
ajv "^6.12.4" ajv "^6.12.4"
debug "^4.3.2" debug "^4.3.2"
@ -1270,10 +1316,10 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/bootstrap@^5.2.2": "@types/bootstrap@^5.2.4":
version "5.2.3" version "5.2.4"
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.3.tgz#562defe61dac6c9598843d4088de8007a36607bd" resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.2.4.tgz#7f4f4af8e22af8247385549bd2f687088d00d2d3"
integrity sha512-r2SE9NYaaI7B/jJk8gqRtXzlhgFL6dlXBResJkCbQa8ept619WeiOIO4zBQxdmUFzkKNWLK5ZOyYGI3QZoaqbQ== integrity sha512-jGNB81zuDHu1DPvBV7Ox3Z3eyzdWPNguYwrt0j7X90VExA8H7c6qxJh0cz5j3xp0XvSy1TYaP2pkyXCHeo8CaA==
dependencies: dependencies:
"@popperjs/core" "^2.9.2" "@popperjs/core" "^2.9.2"
@ -1372,10 +1418,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.4.tgz#fd26723a8a3f8f46729812a7f9b4fc2d1608ed39"
integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg== integrity sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==
"@types/node@^18.7.15": "@types/node@^18.7.16":
version "18.7.15" version "18.7.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.15.tgz#20ae1ec80c57ee844b469f968a1cd511d4088b29" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.16.tgz#0eb3cce1e37c79619943d2fd903919fc30850601"
integrity sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ== integrity sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.1" version "2.4.1"
@ -1466,14 +1512,14 @@
"@typescript-eslint/typescript-estree" "5.32.0" "@typescript-eslint/typescript-estree" "5.32.0"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/parser@^5.36.1": "@typescript-eslint/parser@^5.36.2":
version "5.36.1" version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.1.tgz#931c22c7bacefd17e29734628cdec8b2acdcf1ce" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.2.tgz#3ddf323d3ac85a25295a55fcb9c7a49ab4680ddd"
integrity sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A== integrity sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "5.36.1" "@typescript-eslint/scope-manager" "5.36.2"
"@typescript-eslint/types" "5.36.1" "@typescript-eslint/types" "5.36.2"
"@typescript-eslint/typescript-estree" "5.36.1" "@typescript-eslint/typescript-estree" "5.36.2"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/scope-manager@5.29.0": "@typescript-eslint/scope-manager@5.29.0":
@ -1492,13 +1538,13 @@
"@typescript-eslint/types" "5.32.0" "@typescript-eslint/types" "5.32.0"
"@typescript-eslint/visitor-keys" "5.32.0" "@typescript-eslint/visitor-keys" "5.32.0"
"@typescript-eslint/scope-manager@5.36.1": "@typescript-eslint/scope-manager@5.36.2":
version "5.36.1" version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.1.tgz#23c49b7ddbcffbe09082e6694c2524950766513f" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz#a75eb588a3879ae659514780831370642505d1cd"
integrity sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w== integrity sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw==
dependencies: dependencies:
"@typescript-eslint/types" "5.36.1" "@typescript-eslint/types" "5.36.2"
"@typescript-eslint/visitor-keys" "5.36.1" "@typescript-eslint/visitor-keys" "5.36.2"
"@typescript-eslint/type-utils@5.29.0": "@typescript-eslint/type-utils@5.29.0":
version "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" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.32.0.tgz#484273021eeeae87ddb288f39586ef5efeb6dcd8"
integrity sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ== integrity sha512-EBUKs68DOcT/EjGfzywp+f8wG9Zw6gj6BjWu7KV/IYllqKJFPlZlLSYw/PTvVyiRw50t6wVbgv4p9uE2h6sZrQ==
"@typescript-eslint/types@5.36.1": "@typescript-eslint/types@5.36.2":
version "5.36.1" version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.1.tgz#1cf0e28aed1cb3ee676917966eb23c2f8334ce2c" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.2.tgz#a5066e500ebcfcee36694186ccc57b955c05faf9"
integrity sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg== integrity sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ==
"@typescript-eslint/typescript-estree@5.29.0": "@typescript-eslint/typescript-estree@5.29.0":
version "5.29.0" version "5.29.0"
@ -1550,13 +1596,13 @@
semver "^7.3.7" semver "^7.3.7"
tsutils "^3.21.0" tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.36.1": "@typescript-eslint/typescript-estree@5.36.2":
version "5.36.1" version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.1.tgz#b857f38d6200f7f3f4c65cd0a5afd5ae723f2adb" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz#0c93418b36c53ba0bc34c61fe9405c4d1d8fe560"
integrity sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g== integrity sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w==
dependencies: dependencies:
"@typescript-eslint/types" "5.36.1" "@typescript-eslint/types" "5.36.2"
"@typescript-eslint/visitor-keys" "5.36.1" "@typescript-eslint/visitor-keys" "5.36.2"
debug "^4.3.4" debug "^4.3.4"
globby "^11.1.0" globby "^11.1.0"
is-glob "^4.0.3" is-glob "^4.0.3"
@ -1591,12 +1637,12 @@
"@typescript-eslint/types" "5.32.0" "@typescript-eslint/types" "5.32.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.36.1": "@typescript-eslint/visitor-keys@5.36.2":
version "5.36.1" version "5.36.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.1.tgz#7731175312d65738e501780f923896d200ad1615" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz#2f8f78da0a3bad3320d2ac24965791ac39dace5a"
integrity sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ== integrity sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A==
dependencies: dependencies:
"@typescript-eslint/types" "5.36.1" "@typescript-eslint/types" "5.36.2"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1": "@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
@ -1855,47 +1901,47 @@
semver "^7.3.4" semver "^7.3.4"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"@vue/compiler-core@3.2.38": "@vue/compiler-core@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.38.tgz#0a2a7bffd2280ac19a96baf5301838a2cf1964d7" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.39.tgz#0d77e635f4bdb918326669155a2dc977c053943e"
integrity sha512-/FsvnSu7Z+lkd/8KXMa4yYNUiqQrI22135gfsQYVGuh5tqEgOB0XqrUdb/KnCLa5+TmQLPwvyUnKMyCpu+SX3Q== integrity sha512-mf/36OWXqWn0wsC40nwRRGheR/qoID+lZXbIuLnr4/AngM0ov8Xvv8GHunC0rKRIkh60bTqydlqTeBo49rlbqw==
dependencies: dependencies:
"@babel/parser" "^7.16.4" "@babel/parser" "^7.16.4"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map "^0.6.1" source-map "^0.6.1"
"@vue/compiler-dom@3.2.38": "@vue/compiler-dom@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.38.tgz#53d04ed0c0c62d1ef259bf82f9b28100a880b6fd" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.39.tgz#bd69d35c1a48fe2cea4ab9e96d2a3a735d146fdf"
integrity sha512-zqX4FgUbw56kzHlgYuEEJR8mefFiiyR3u96498+zWPsLeh1WKvgIReoNE+U7gG8bCUdvsrJ0JRmev0Ky6n2O0g== integrity sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==
dependencies: dependencies:
"@vue/compiler-core" "3.2.38" "@vue/compiler-core" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
"@vue/compiler-sfc@3.2.38": "@vue/compiler-sfc@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.38.tgz#9e763019471a535eb1fceeaac9d4d18a83f0940f" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.39.tgz#8fe29990f672805b7c5a2ecfa5b05e681c862ea2"
integrity sha512-KZjrW32KloMYtTcHAFuw3CqsyWc5X6seb8KbkANSWt3Cz9p2qA8c1GJpSkksFP9ABb6an0FLCFl46ZFXx3kKpg== integrity sha512-fqAQgFs1/BxTUZkd0Vakn3teKUt//J3c420BgnYgEOoVdTwYpBTSXCMJ88GOBCylmUBbtquGPli9tVs7LzsWIA==
dependencies: dependencies:
"@babel/parser" "^7.16.4" "@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.38" "@vue/compiler-core" "3.2.39"
"@vue/compiler-dom" "3.2.38" "@vue/compiler-dom" "3.2.39"
"@vue/compiler-ssr" "3.2.38" "@vue/compiler-ssr" "3.2.39"
"@vue/reactivity-transform" "3.2.38" "@vue/reactivity-transform" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.25.7" magic-string "^0.25.7"
postcss "^8.1.10" postcss "^8.1.10"
source-map "^0.6.1" source-map "^0.6.1"
"@vue/compiler-ssr@3.2.38": "@vue/compiler-ssr@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.38.tgz#933b23bf99e667e5078eefc6ba94cb95fd765dfe" resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.39.tgz#4f3bfb535cb98b764bee45e078700e03ccc60633"
integrity sha512-bm9jOeyv1H3UskNm4S6IfueKjUNFmi2kRweFIGnqaGkkRePjwEcfCVqyS3roe7HvF4ugsEkhf4+kIvDhip6XzQ== integrity sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==
dependencies: dependencies:
"@vue/compiler-dom" "3.2.38" "@vue/compiler-dom" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.3.0": "@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.3.0":
version "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" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ== integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
"@vue/eslint-config-typescript@^11.0.0": "@vue/eslint-config-typescript@^11.0.1":
version "11.0.0" version "11.0.1"
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.0.tgz#bac0cb2d381625b5bf568d2025acffc0fd09113e" resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.1.tgz#d79b3656aecea844ec9875bc93155163f684dde7"
integrity sha512-txuRzxnQVmtUvvy9UyWUy9sHWXNeRPGmSPqP53hRtaiUeCTAondI9Ho9GQYI/8/eWljYOST7iA4Aa8sANBkWaA== integrity sha512-0U+nL0nA7ahnGPk3rTN49x76miUwuQtQPQNWOFvAcjg6nFJkIkA8qbGNtXwsuHtwBwRtWpHhShL3zK07v+632w==
dependencies: dependencies:
"@typescript-eslint/eslint-plugin" "^5.0.0" "@typescript-eslint/eslint-plugin" "^5.0.0"
"@typescript-eslint/parser" "^5.0.0" "@typescript-eslint/parser" "^5.0.0"
vue-eslint-parser "^9.0.0" vue-eslint-parser "^9.0.0"
"@vue/reactivity-transform@3.2.38": "@vue/reactivity-transform@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.38.tgz#a856c217b2ead99eefb6fddb1d61119b2cb67984" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz#da6ae6c8fd77791b9ae21976720d116591e1c4aa"
integrity sha512-3SD3Jmi1yXrDwiNJqQ6fs1x61WsDLqVk4NyKVz78mkaIRh6d3IqtRnptgRfXn+Fzf+m6B1KxBYWq1APj6h4qeA== integrity sha512-HGuWu864zStiWs9wBC6JYOP1E00UjMdDWIG5W+FpUx28hV3uz9ODOKVNm/vdOy/Pvzg8+OcANxAVC85WFBbl3A==
dependencies: dependencies:
"@babel/parser" "^7.16.4" "@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.38" "@vue/compiler-core" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.25.7" magic-string "^0.25.7"
"@vue/reactivity@3.2.38": "@vue/reactivity@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.38.tgz#d576fdcea98eefb96a1f1ad456e289263e87292e" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.39.tgz#e6e3615fe2288d4232b104640ddabd0729a78c80"
integrity sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw== integrity sha512-vlaYX2a3qMhIZfrw3Mtfd+BuU+TZmvDrPMa+6lpfzS9k/LnGxkSuf0fhkP0rMGfiOHPtyKoU9OJJJFGm92beVQ==
dependencies: dependencies:
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
"@vue/runtime-core@3.2.38": "@vue/runtime-core@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.38.tgz#d19cf591c210713f80e6a94ffbfef307c27aea06" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.39.tgz#dc1faccab11b3e81197aba33fb30c9447c1d2c84"
integrity sha512-kk0qiSiXUU/IKxZw31824rxmFzrLr3TL6ZcbrxWTKivadoKupdlzbQM4SlGo4MU6Zzrqv4fzyUasTU1jDoEnzg== integrity sha512-xKH5XP57JW5JW+8ZG1khBbuLakINTgPuINKL01hStWLTTGFOrM49UfCFXBcFvWmSbci3gmJyLl2EAzCaZWsx8g==
dependencies: dependencies:
"@vue/reactivity" "3.2.38" "@vue/reactivity" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
"@vue/runtime-dom@3.2.38": "@vue/runtime-dom@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.38.tgz#fec711f65c2485991289fd4798780aa506469b48" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.39.tgz#4a8cb132bcef316e8151c5ed07fc7272eb064614"
integrity sha512-4PKAb/ck2TjxdMSzMsnHViOrrwpudk4/A56uZjhzvusoEU9xqa5dygksbzYepdZeB5NqtRw5fRhWIiQlRVK45A== integrity sha512-4G9AEJP+sLhsqf5wXcyKVWQKUhI+iWfy0hWQgea+CpaTD7BR0KdQzvoQdZhwCY6B3oleSyNLkLAQwm0ya/wNoA==
dependencies: dependencies:
"@vue/runtime-core" "3.2.38" "@vue/runtime-core" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
csstype "^2.6.8" csstype "^2.6.8"
"@vue/server-renderer@3.2.38": "@vue/server-renderer@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.38.tgz#01a4c0f218e90b8ad1815074208a1974ded109aa" resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.39.tgz#4358292d925233b0d8b54cf0513eaece8b2351c5"
integrity sha512-pg+JanpbOZ5kEfOZzO2bt02YHd+ELhYP8zPeLU1H0e7lg079NtuuSB8fjLdn58c4Ou8UQ6C1/P+528nXnLPAhA== integrity sha512-1yn9u2YBQWIgytFMjz4f/t0j43awKytTGVptfd3FtBk76t1pd8mxbek0G/DrnjJhd2V7mSTb5qgnxMYt8Z5iSQ==
dependencies: dependencies:
"@vue/compiler-ssr" "3.2.38" "@vue/compiler-ssr" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
"@vue/shared@3.2.38": "@vue/shared@3.2.39":
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.38.tgz#e823f0cb2e85b6bf43430c0d6811b1441c300f3c" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.39.tgz#302df167559a1a5156da162d8cc6760cef67f8e3"
integrity sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg== integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
"@vue/vue-loader-v15@npm:vue-loader@^15.9.7": "@vue/vue-loader-v15@npm:vue-loader@^15.9.7":
version "15.9.8" 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" resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.8.1.tgz#ce4a0c1f6efe41dabcc1341f2cb191d307fbaf50"
integrity sha512-uItRULwQz0epETi9x/RBEqfjHmTAmkIIczpH1R6L9T6yyaaijk0826PzTWnWNm15tw66JT/8GNuXjB0HI5PHLw== integrity sha512-uItRULwQz0epETi9x/RBEqfjHmTAmkIIczpH1R6L9T6yyaaijk0826PzTWnWNm15tw66JT/8GNuXjB0HI5PHLw==
bootstrap@^5.2.0: bootstrap@^5.2.1:
version "5.2.0" version "5.2.1"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.0.tgz#838727fb60f1630db370fe57c63cbcf2962bb3d3" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.1.tgz#45f97ff05cbe828bad807b014d8425f3aeb8ec3a"
integrity sha512-qlnS9GL6YZE6Wnef46GxGv1UpGGzAwO0aPL1yOjzDIJpeApeMvqV24iL+pjr2kU4dduoBA9fINKWKgMToobx9A== integrity sha512-UQi3v2NpVPEi1n35dmRRzBJFlgvWHYwyem6yHhuT6afYF+sziEt46McRbT//kVXZ7b1YUYEVGdXEH74Nx3xzGA==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" 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" browserslist "^4.20.3"
semver "7.0.0" semver "7.0.0"
core-js@^3.25.0: core-js@^3.25.1:
version "3.25.0" version "3.25.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.0.tgz#be71d9e0dd648ffd70c44a7ec2319d039357eceb" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.1.tgz#5818e09de0db8956e16bf10e2a7141e931b7c69c"
integrity sha512-CVU1xvJEfJGhyCpBrzzzU1kjCfgsGUxhEvwUV2e/cOedYWHdmluamx+knDnmhqALddMG16fZvIqvs9aijsHHaA== integrity sha512-sr0FY4lnO1hkQ4gLDr24K0DGnweGO1QwSj5BpfQjpSJPdqWalja4cTps29Y/PJVG/P7FYlPDkH3hO+Tr0CvDgQ==
core-js@^3.8.3: core-js@^3.8.3:
version "3.24.1" version "3.24.1"
@ -3293,12 +3339,12 @@ eslint-webpack-plugin@^3.1.0:
normalize-path "^3.0.0" normalize-path "^3.0.0"
schema-utils "^3.1.1" schema-utils "^3.1.1"
eslint@^8.23.0: eslint@^8.23.1:
version "8.23.0" version "8.23.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.0.tgz#a184918d288820179c6041bb3ddcc99ce6eea040" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.23.1.tgz#cfd7b3f7fdd07db8d16b4ac0516a29c8d8dca5dc"
integrity sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA== integrity sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==
dependencies: dependencies:
"@eslint/eslintrc" "^1.3.1" "@eslint/eslintrc" "^1.3.2"
"@humanwhocodes/config-array" "^0.10.4" "@humanwhocodes/config-array" "^0.10.4"
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2" "@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
"@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/module-importer" "^1.0.1"
@ -3317,7 +3363,6 @@ eslint@^8.23.0:
fast-deep-equal "^3.1.3" fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1" file-entry-cache "^6.0.1"
find-up "^5.0.0" find-up "^5.0.0"
functional-red-black-tree "^1.0.1"
glob-parent "^6.0.1" glob-parent "^6.0.1"
globals "^13.15.0" globals "^13.15.0"
globby "^11.1.0" globby "^11.1.0"
@ -3326,6 +3371,7 @@ eslint@^8.23.0:
import-fresh "^3.0.0" import-fresh "^3.0.0"
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
is-glob "^4.0.0" is-glob "^4.0.0"
js-sdsl "^4.1.4"
js-yaml "^4.1.0" js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1" json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.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" resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47"
integrity sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA== 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: js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 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" media-typer "0.3.0"
mime-types "~2.1.24" mime-types "~2.1.24"
typescript@^4.8.2: typescript@^4.8.3:
version "4.8.2" version "4.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88"
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==
unicode-canonical-property-names-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript@^2.0.0:
version "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" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@^3.2.38: vue@^3.2.39:
version "3.2.38" version "3.2.39"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.38.tgz#cda3a414631745b194971219318a792dbbccdec0" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.39.tgz#de071c56c4c32c41cbd54e55f11404295c0dd62d"
integrity sha512-hHrScEFSmDAWL0cwO4B6WO7D3sALZPbfuThDsGBebthrNlDxdJZpGR3WB87VbjpPh96mep1+KzukYEhpHDFa8Q== integrity sha512-tRkguhRTw9NmIPXhzk21YFBqXHT2t+6C6wPOgQ50fcFVWnPdetmRqbmySRHznrYjX2E47u0cGlKGcxKZJ38R/g==
dependencies: dependencies:
"@vue/compiler-dom" "3.2.38" "@vue/compiler-dom" "3.2.39"
"@vue/compiler-sfc" "3.2.38" "@vue/compiler-sfc" "3.2.39"
"@vue/runtime-dom" "3.2.38" "@vue/runtime-dom" "3.2.39"
"@vue/server-renderer" "3.2.38" "@vue/server-renderer" "3.2.39"
"@vue/shared" "3.2.38" "@vue/shared" "3.2.39"
watchpack@^2.3.1: watchpack@^2.3.1:
version "2.3.1" version "2.3.1"

Binary file not shown.