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

21
.github/workflows/cpplint.yml vendored Normal file
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 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
This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net).
It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed.
@ -55,6 +58,7 @@ Sends text raw data as difined in VE.Direct spec.
* Read live data from inverter
* Show inverters internal event log
* Show inverter information like firmware version, firmware build date, hardware revision and hardware version
* Show current inverter limit (setting the limit is not yet implemented)
* Uses ESP32 microcontroller and NRF24L01+
* Multi-Inverter support
* MQTT support (with TLS)
@ -75,6 +79,39 @@ Sends text raw data as difined in VE.Direct spec.
* Build with [Vue.js](https://vuejs.org)
* Source is written in TypeScript
## Hardware you need
### ESP32 board
For ease of use, buy a "ESP32 DEVKIT DOIT" or "ESP32 NodeMCU Development Board" with an ESP32-S3 or ESP-WROOM-32 chipset on it.
Sample Picture:
![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
### Schematic
![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
### with Visual Studio Code
* Install [Visual Studio Code](https://code.visualstudio.com/download)
* Install [Visual Studio Code](https://code.visualstudio.com/download) (from now named "vscode")
* In Visual Studio Code, install the [PlatformIO Extension](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide)
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur)
* In Visual Studio Code, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" file)
* Adjust the COM port in the file "platformio.ini". It occurs twice:
* Install git and enable git in vscode - [git download](https://git-scm.com/downloads/) - [Instructions](https://www.jcchouinard.com/install-git-in-vscode/)
* Clone this repository (you really have to clone it, don't just download the ZIP file. During the build process the git hash gets embedded into the firmware. If you download the ZIP file a build error will occur): Inside vscode open the command palette by pressing `CTRL` + `SHIFT` + `P`. Enter `git clone`, add the repository-URL `https://github.com/tbnobody/OpenDTU`. Next you have to choose (or create) a target directory.
* In vscode, choose File --> Open Folder and select the previously downloaded source code. (You have to select the folder which contains the "platformio.ini" file)
* There is a short [Video](https://youtu.be/9cA_esv3zeA) showing these steps.
* Adjust the COM port in the file "platformio.ini" for your USB-serial-converter. It occurs twice:
* upload_port
* monitor_port
* Select the arrow button in the status bar (PlatformIO: Upload) to compile and upload the firmware. During the compilation, all required libraries are downloaded automatically.
@ -152,3 +191,21 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw
* Building the microcontroller firmware
* Visual Studio Code with the PlatformIO Extension is required for building
## Troubleshooting
* First: When there is no light on the solar panels, the inverter completely turns off and does not answer to OpenDTU! So if you assembled your OpenDTU in the evening, wait until tomorrow.
* When there is no data received from the inverter(s) - try to reduce the distance between the openDTU and the inverter (e.g. move it to the window towards the roof)
* Under Settings -> DTU Settings you can increase the transmit power "PA level". Default is "minimum".
* The NRF24L01+ needs relatively much current. With bad power supply (and especially bad cables!) a 10uF capacitor soldered directly to the NRF24L01+ board connector brings more stability (pin 1+2 are the power supply). Note the polarity of the capacitor....
* You can try to use an USB power supply with 1A or more instead of connecting the ESP32 to the computer.
* Try a different USB cable. Once again, a stable power source is important. Some USB cables are made of much plastic and very little copper inside.
* Double-Check that you have a radio module NRF24L01+ with a plus sign at the end. NRF24L01 module without the plus are not compatible with this project.
* There is no possibility of auto-discovering the inverters. Double-Check you have entered the serial numbers of the inverters correctly.
* OpenDTU needs access to a working NTP server to get the current date & time.
* If your problem persists, check the [Issues on Github](https://github.com/tbnobody/OpenDTU/issues). Please inspect not only the open issues, also the closed issues contain useful information.
* Another source of information are the [Discussions](https://github.com/tbnobody/OpenDTU/discussions/)
## Related Projects
- [Ahoy](https://github.com/grindylow/ahoy)
- [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles)
- [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect)

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

View File

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

18
include/WebApi_config.h Normal file
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 onMqttAdminGet(AsyncWebServerRequest* request);
void onMqttAdminPost(AsyncWebServerRequest* request);
String getRootCaCertInfo(char* cert);
String getRootCaCertInfo(const char* cert);
AsyncWebServer* _server;
};

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
class AlarmDataCommand : public MultiDataCommand {
public:
AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
explicit AlarmDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
};

View File

@ -10,7 +10,7 @@ class InverterAbstract;
class CommandAbstract {
public:
CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0);
explicit CommandAbstract(uint64_t target_address = 0, uint64_t router_address = 0);
virtual ~CommandAbstract() {};
template <typename T>

View File

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

View File

@ -4,7 +4,7 @@
class DevInfoAllCommand : public MultiDataCommand {
public:
DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
explicit DevInfoAllCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
};

View File

@ -4,7 +4,7 @@
class DevInfoSampleCommand : public MultiDataCommand {
public:
DevInfoSampleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
explicit DevInfoSampleCommand(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0);
virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id);
};

View File

@ -6,7 +6,7 @@
class MultiDataCommand : public CommandAbstract {
public:
MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0);
explicit MultiDataCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t data_type = 0, time_t time = 0);
void setTime(time_t time);
time_t getTime();

View File

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

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

View File

@ -4,7 +4,7 @@
class RequestFrameCommand : public SingleDataCommand {
public:
RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0);
explicit RequestFrameCommand(uint64_t target_address = 0, uint64_t router_address = 0, uint8_t frame_no = 0);
void setFrameNo(uint8_t frame_no);
uint8_t getFrameNo();

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
#include "commands/DevInfoAllCommand.h"
#include "commands/DevInfoSampleCommand.h"
#include "commands/RealTimeRunDataCommand.h"
#include "commands/SystemConfigParaCommand.h"
HM_Abstract::HM_Abstract(uint64_t serial)
: InverterAbstract(serial) {};
@ -68,5 +69,22 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio)
cmdSample->setTime(now);
cmdSample->setTargetAddress(serial());
return true;
}
bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio)
{
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
return false;
}
time_t now;
time(&now);
SystemConfigParaCommand* cmd = radio->enqueCommand<SystemConfigParaCommand>();
cmd->setTime(now);
cmd->setTargetAddress(serial());
return true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -1,68 +1,91 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = generic
[env]
framework = arduino
platform = espressif32@>=5
build_flags =
-D=${PIOENV}
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
lib_deps =
https://github.com/me-no-dev/ESPAsyncWebServer.git
bblanchon/ArduinoJson @ ^6.19.4
https://github.com/bertmelis/espMqttClient.git
nrf24/RF24 @ ^1.4.5
extra_scripts = pre:auto_firmware_version.py
board_build.partitions = partitions_custom.csv
board_build.filesystem = littlefs
monitor_filters =
time
colorize
log2file
esp32_exception_decoder
monitor_speed = 115200
upload_protocol = esptool
[env:generic]
board = esp32dev
monitor_port = COM4
upload_port = COM4
[env:olimex_esp32_poe]
board = esp32-poe
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=15
-DHOYMILES_PIN_MOSI=2
-DHOYMILES_PIN_SCLK=14
-DHOYMILES_PIN_IRQ=13
-DHOYMILES_PIN_CE=16
-DHOYMILES_PIN_CS=5
-DOPENDTU_ETHERNET
monitor_port = COM3
upload_port = COM3
[env:d1 mini esp32]
board = wemos_d1_mini32
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
monitor_port = /dev/cu.usbserial-01E68DD0
upload_port = /dev/cu.usbserial-01E68DD0
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = generic
[env]
framework = arduino
platform = espressif32@>=5
build_flags =
-D=${PIOENV}
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.19.4
https://github.com/bertmelis/espMqttClient.git#v1.2.3
nrf24/RF24 @ ^1.4.5
extra_scripts =
pre:auto_firmware_version.py
board_build.partitions = partitions_custom.csv
board_build.filesystem = littlefs
monitor_filters = time, colorize, log2file, esp32_exception_decoder
monitor_speed = 115200
upload_protocol = esptool
[env:generic]
board = esp32dev
monitor_port = COM4
upload_port = COM4
[env:olimex_esp32_poe]
; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware
board = esp32-poe
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=15
-DHOYMILES_PIN_MOSI=2
-DHOYMILES_PIN_SCLK=14
-DHOYMILES_PIN_IRQ=13
-DHOYMILES_PIN_CE=16
-DHOYMILES_PIN_CS=5
-DOPENDTU_ETHERNET
monitor_port = COM3
upload_port = COM3
[env:olimex_esp32_evb]
; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware
board = esp32-evb
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=15
-DHOYMILES_PIN_MOSI=2
-DHOYMILES_PIN_SCLK=14
-DHOYMILES_PIN_IRQ=13
-DHOYMILES_PIN_CE=16
-DHOYMILES_PIN_CS=17
-DOPENDTU_ETHERNET
monitor_port = /dev/tty.usbserial-1450
upload_port = /dev/tty.usbserial-1450
[env:d1 mini esp32]
board = wemos_d1_mini32
build_flags =
${env.build_flags}
-DHOYMILES_PIN_MISO=19
-DHOYMILES_PIN_MOSI=23
-DHOYMILES_PIN_SCLK=18
-DHOYMILES_PIN_IRQ=16
-DHOYMILES_PIN_CE=17
-DHOYMILES_PIN_CS=5
-DVICTRON_PIN_TX=21
-DVICTRON_PIN_RX=22
monitor_port = /dev/cu.usbserial-01E68DD0
upload_port = /dev/cu.usbserial-01E68DD0

View File

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

View File

@ -46,7 +46,7 @@ void MqttHassPublishingClass::publishConfig()
return;
}
CONFIG_T& config = Configuration.get();
const CONFIG_T& config = Configuration.get();
// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
@ -74,7 +74,7 @@ void MqttHassPublishingClass::publishField(std::shared_ptr<InverterAbstract> inv
}
char serial[sizeof(uint64_t) * 8 + 1];
sprintf(serial, "%0lx%08lx",
snprintf(serial, sizeof(serial), "%0lx%08lx",
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

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

View File

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

View File

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

View File

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

125
src/WebApi_config.cpp Normal file
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)
{
using namespace std::placeholders;
using std::placeholders::_1;
_server = server;
@ -31,7 +31,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
// Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
sprintf(buffer, "%0lx%08lx",
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

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

View File

@ -9,7 +9,7 @@
void WebApiEventlogClass::init(AsyncWebServer* server)
{
using namespace std::placeholders;
using std::placeholders::_1;
_server = server;
@ -22,15 +22,21 @@ void WebApiEventlogClass::loop()
void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse();
AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048);
JsonObject root = response->getRoot();
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
uint64_t serial = 0;
if (request->hasParam("inv")) {
String s = request->getParam("inv")->value();
serial = strtoll(s.c_str(), NULL, 16);
}
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
// Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
sprintf(buffer, "%0lx%08lx",
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));

View File

@ -11,7 +11,12 @@
void WebApiFirmwareClass::init(AsyncWebServer* server)
{
using namespace std::placeholders;
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
_server = server;

View File

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

42
src/WebApi_limit.cpp Normal file
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)
{
using namespace std::placeholders;
using std::placeholders::_1;
_server = server;
@ -29,7 +29,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot();
CONFIG_T& config = Configuration.get();
const CONFIG_T& config = Configuration.get();
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
@ -56,7 +56,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
JsonObject root = response->getRoot();
CONFIG_T& config = Configuration.get();
const CONFIG_T& config = Configuration.get();
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
@ -240,21 +240,21 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
config.Mqtt_Enabled = root[F("mqtt_enabled")].as<bool>();
config.Mqtt_Retain = root[F("mqtt_retain")].as<bool>();
config.Mqtt_Tls = root[F("mqtt_tls")].as<bool>();
strcpy(config.Mqtt_RootCaCert, root[F("mqtt_root_ca_cert")].as<String>().c_str());
strlcpy(config.Mqtt_RootCaCert, root[F("mqtt_root_ca_cert")].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert));
config.Mqtt_Port = root[F("mqtt_port")].as<uint>();
strcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str());
strcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str());
strcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str());
strcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str());
strcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str());
strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str());
strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str());
strlcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str(), sizeof(config.Mqtt_Hostname));
strlcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str(), sizeof(config.Mqtt_Username));
strlcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str(), sizeof(config.Mqtt_Password));
strlcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Mqtt_Topic));
strlcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str(), sizeof(config.Mqtt_LwtTopic));
strlcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Online));
strlcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Offline));
config.Mqtt_PublishInterval = root[F("mqtt_publish_interval")].as<uint32_t>();
config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as<bool>();
config.Mqtt_Hass_Expire = root[F("mqtt_hass_expire")].as<bool>();
config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>();
config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as<bool>();
strcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str());
strlcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str(), sizeof(config.Mqtt_Hass_Topic));
Configuration.write();
retMsg[F("type")] = F("success");
@ -267,18 +267,18 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
MqttHassPublishing.forceUpdate();
}
String WebApiMqttClass::getRootCaCertInfo(char* cert)
String WebApiMqttClass::getRootCaCertInfo(const char* cert)
{
char rootCaCertInfo[1024] = "";
mbedtls_x509_crt global_cacert;
strcpy(rootCaCertInfo, "Can't parse root ca");
strlcpy(rootCaCertInfo, "Can't parse root ca", sizeof(rootCaCertInfo));
mbedtls_x509_crt_init(&global_cacert);
int ret = mbedtls_x509_crt_parse(&global_cacert, const_cast<unsigned char*>((unsigned char*)cert), 1 + strlen(cert));
if (ret < 0) {
sprintf(rootCaCertInfo, "Can't parse root ca: mbedtls_x509_crt_parse returned -0x%x\n\n", -ret);
snprintf(rootCaCertInfo, sizeof(rootCaCertInfo), "Can't parse root ca: mbedtls_x509_crt_parse returned -0x%x\n\n", -ret);
mbedtls_x509_crt_free(&global_cacert);
return "";
}

View File

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

View File

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

View File

@ -16,7 +16,7 @@
void WebApiSysstatusClass::init(AsyncWebServer* server)
{
using namespace std::placeholders;
using std::placeholders::_1;
_server = server;
@ -58,7 +58,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount;
char version[16];
sprintf(version, "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
root[F("firmware_version")] = version;
root[F("git_hash")] = AUTO_GIT_HASH;

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

View File

@ -13,7 +13,12 @@ WebApiWsLiveClass::WebApiWsLiveClass()
void WebApiWsLiveClass::init(AsyncWebServer* server)
{
using namespace std::placeholders;
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
_server = server;
_server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1));
@ -56,10 +61,9 @@ void WebApiWsLiveClass::loop()
JsonVariant var = root;
generateJsonResponse(var);
size_t len = measureJson(root);
AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); // creates a buffer (len + 1) for you.
String buffer;
if (buffer) {
serializeJson(root, (char*)buffer->get(), len + 1);
serializeJson(root, buffer);
_ws.textAll(buffer);
}
@ -74,7 +78,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
auto inv = Hoymiles.getInverterByPos(i);
char buffer[sizeof(uint64_t) * 8 + 1];
sprintf(buffer, "%0lx%08lx",
snprintf(buffer, sizeof(buffer), "%0lx%08lx",
((uint32_t)((inv->serial() >> 32) & 0xFFFFFFFF)),
((uint32_t)(inv->serial() & 0xFFFFFFFF)));
@ -135,11 +139,11 @@ void WebApiWsLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketC
{
if (type == WS_EVT_CONNECT) {
char str[64];
sprintf(str, "Websocket: [%s][%u] connect", server->url(), client->id());
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
Serial.println(str);
} else if (type == WS_EVT_DISCONNECT) {
char str[64];
sprintf(str, "Websocket: [%s][%u] disconnect", server->url(), client->id());
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
Serial.println(str);
}
}

View File

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

View File

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

View File

@ -1,10 +1,101 @@
<template>
<div class="container-xxl" role="main">
<div class="page-header">
<h1>About</h1>
This project was started from
<a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion.
(Mikrocontroller.net)</a>
<h1>About OpenDTU</h1>
</div>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<span class="badge bg-secondary">
<BIconInfoCircle class="fs-4" />
</span>&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
<a href="https://www.mikrocontroller.net/topic/525778" target="_blank">this discussion.
(Mikrocontroller.net)</a>
</p>
<p class="fw-normal">
The Hoymiles protocol was decrypted through the voluntary efforts of many participants.
OpenDTU,
among others, was developed based on this work. The project is licensed under an Open Source
License (<a href="https://www.gnu.de/documents/gpl-2.0.de.html" target="_blank">GNU General
Public License version 2</a>).
</p>
<p class="fw-normal">
The software was developed to the best of our knowledge and belief. Nevertheless, no
liability
can be accepted for a malfunction or guarantee loss of the inverter.
</p>
<p class="fw-normal">
OpenDTU is freely available. If you paid money for the software, you probably got ripped
off.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<span class="badge bg-secondary">
<BIconActivity class="fs-4" />
</span>&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>

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)
<div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-danger"
@click="onShowLimitSettings(inverter.serial)" title="Show / Set Inverter Limit">
<BIconSpeedometer style="font-size:24px;" />
</button>
</div>
<div class="btn-group me-2" role="group">
<button type="button" class="btn btn-sm btn-info"
@click="onShowDevInfo(inverter.serial)" title="Show Inverter Info">
@ -130,6 +138,30 @@
</div>
</div>
<div class="modal" id="limitSettingView" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Limit Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center" v-if="limitSettingLoading">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<LimitSettingsCurrent v-if="!limitSettingLoading" :limitData="limitSettingList" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="onHideLimitSettings"
data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</template>
@ -139,6 +171,7 @@ import InverterChannelInfo from "@/components/partials/InverterChannelInfo.vue";
import * as bootstrap from 'bootstrap';
import EventLog from '@/components/partials/EventLog.vue';
import DevInfo from '@/components/partials/DevInfo.vue';
import LimitSettingsCurrent from '@/components/partials/LimitSettingsCurrent.vue';
import VedirectView from '@/components/partials/VedirectView.vue';
declare interface Inverter {
@ -154,6 +187,7 @@ export default defineComponent({
InverterChannelInfo,
EventLog,
DevInfo,
LimitSettingsCurrent,
VedirectView
},
data() {
@ -169,7 +203,10 @@ export default defineComponent({
eventLogLoading: true,
devInfoView: {} as bootstrap.Modal,
devInfoList: {},
devInfoLoading: true
devInfoLoading: true,
limitSettingView: {} as bootstrap.Modal,
limitSettingList: {},
limitSettingLoading: true,
};
},
created() {
@ -180,6 +217,7 @@ export default defineComponent({
mounted() {
this.eventLogView = new bootstrap.Modal('#eventView');
this.devInfoView = new bootstrap.Modal('#devInfoView');
this.limitSettingView = new bootstrap.Modal('#limitSettingView');
},
unmounted() {
this.closeSocket();
@ -264,7 +302,7 @@ export default defineComponent({
},
onShowEventlog(serial: number) {
this.eventLogLoading = true;
fetch("/api/eventlog/status")
fetch("/api/eventlog/status?inv=" + serial)
.then((response) => response.json())
.then((data) => {
this.eventLogList = data[serial];
@ -287,6 +325,20 @@ export default defineComponent({
this.devInfoView.show();
},
onHideLimitSettings() {
this.limitSettingView.hide();
},
onShowLimitSettings(serial: number) {
this.limitSettingLoading = true;
fetch("/api/limit/status")
.then((response) => response.json())
.then((data) => {
this.limitSettingList = data[serial];
this.limitSettingLoading = false;
});
this.limitSettingView.show();
},
},
});
</script>

View File

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

View File

@ -39,6 +39,11 @@
<div class="col-sm-10">
<input type="text" class="form-control" id="inputHostname" maxlength="32"
placeholder="Hostname" v-model="networkConfigList.hostname" />
<div class="alert alert-secondary" role="alert">
<b>Hint:</b> The text <span class="font-monospace">%06X</span> will be replaced
with the last 6 digits of the ESP ChipID in hex format.
</div>
</div>
</div>

View File

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

View File

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

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

View File

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

Binary file not shown.