Merge branch 'tbnobody:master' into master

This commit is contained in:
Ralf Bauer 2024-09-29 16:40:58 +02:00 committed by GitHub
commit 2719238db0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 2250 additions and 914 deletions

View File

@ -47,7 +47,8 @@ body:
label: Install Method
description: How did you install OpenDTU?
options:
- Pre-Compiled binary from GitHub
- Pre-Compiled binary from GitHub releases
- Pre-Compiles binary from GitHub actions/pull-request
- Self-Compiled
validations:
required: true
@ -59,6 +60,14 @@ body:
placeholder: "e.g. 359d513"
validations:
required: true
- type: input
id: environment
attributes:
label: What firmware variant (PIO Environment) are you using?
description: You can find this in by going to Info -> System
placeholder: "generic_esp32s3_usb"
validations:
required: true
- type: textarea
id: logs
attributes:
@ -84,5 +93,5 @@ body:
required: true
- label: I have updated the title field above with a concise description.
required: true
- label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported
- label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported.
required: true

View File

@ -43,7 +43,7 @@ jobs:
environments: ${{ steps.envs.outputs.environments }}
build:
name: Build Enviornments
name: Build Environments
runs-on: ubuntu-latest
needs: get_default_envs
strategy:
@ -79,18 +79,27 @@ jobs:
python -m pip install --upgrade pip
pip install --upgrade platformio setuptools
- name: Enable Corepack
run: |
cd webapp
corepack enable
- name: Setup Node.js and yarn
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
cache: "yarn"
cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile
run: |
cd webapp
yarn install --frozen-lockfile
- name: Build WebApp
run: yarn --cwd webapp build
run: |
cd webapp
yarn build
- name: Build firmware
run: pio run -e ${{ matrix.environment }}

View File

@ -18,6 +18,12 @@
"fix"
]
},
{
"title": "## 🌎 Web Application",
"labels": [
"webapp"
]
},
{
"title": "## 📚 Documentation",
"labels": [

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:

View File

@ -6,17 +6,23 @@ jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: webapp
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js and yarn
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "22"
cache: "yarn"
cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies
run: yarn --cwd webapp install --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Linting
run: yarn --cwd webapp lint
run: yarn lint

28
.github/workflows/yarnprettier.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Yarn Prettier
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: webapp
steps:
- uses: actions/checkout@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js and yarn
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "yarn"
cache-dependency-path: "webapp/yarn.lock"
- name: Install WebApp dependencies
run: yarn install --frozen-lockfile
- name: Check Formatting
run: yarn prettier --check src/

View File

@ -43,41 +43,4 @@ Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | gre
## Currently supported Inverters
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
| ---------------------| ------------------ | --------- | ----------- | --------- |
| Hoymiles HM-300-1T | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-350-1T | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-400-1T | NRF24L01+ | 1 | 1 | 1 |
| Hoymiles HM-600-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-700-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-800-2T | NRF24L01+ | 2 | 2 | 1 |
| Hoymiles HM-1000-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1200-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HM-1500-4T | NRF24L01+ | 4 | 2 | 1 |
| Hoymiles HMS-300-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-350-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-400-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-450-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-500-1T | CMT2300A | 1 | 1 | 1 |
| Hoymiles HMS-600-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-700-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-800-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-900-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1000-2T | CMT2300A | 2 | 2 | 1 |
| Hoymiles HMS-1600-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-1800-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMS-2000-4T | CMT2300A | 4 | 4 | 1 |
| Hoymiles HMT-1600-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-1800-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-2000-4T | CMT2300A | 4 | 2 | 3 |
| Hoymiles HMT-1800-6T | CMT2300A | 6 | 3 | 3 |
| Hoymiles HMT-2250-6T | CMT2300A | 6 | 3 | 3 |
| Solenso SOL-H350 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H400 | NRF24L01+ | 1 | 1 | 1 |
| Solenso SOL-H800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M350 | NRF24L01+ | 1 | 1 | 1 |
| TSUN TSOL-M800 | NRF24L01+ | 2 | 2 | 1 |
| TSUN TSOL-M1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-800 | NRF24L01+ | 2 | 2 | 1 |
| E-Star HERF-1600 | NRF24L01+ | 4 | 2 | 1 |
| E-Star HERF-1800 | NRF24L01+ | 4 | 2 | 1 |
A list of all currently supported inverters can be found [here](https://www.opendtu.solar/hardware/inverter_overview/)

View File

@ -1,6 +1,9 @@
[
{
"name": "OpenDTU Fusion v1",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -25,6 +28,9 @@
},
{
"name": "OpenDTU Fusion v1 with SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -54,6 +60,9 @@
},
{
"name": "OpenDTU Fusion v1 with SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -83,6 +92,9 @@
},
{
"name": "OpenDTU Fusion v2 with CMT2300A and NRF24",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -115,6 +127,9 @@
},
{
"name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -152,6 +167,9 @@
},
{
"name": "OpenDTU Fusion v2 with CMT2300A, NRF24 and SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
@ -186,5 +204,44 @@
"data": 2,
"clk": 1
}
},
{
"name": "OpenDTU Fusion v2 PoE",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 0,
"data": 2,
"clk": 1
}
}
]
]

View File

@ -6,7 +6,7 @@
#include <TaskSchedulerDeclarations.h>
// mqtt discovery device classes
enum {
enum DeviceClassType {
DEVICE_CLS_NONE = 0,
DEVICE_CLS_CURRENT,
DEVICE_CLS_ENERGY,
@ -15,20 +15,34 @@ enum {
DEVICE_CLS_FREQ,
DEVICE_CLS_TEMP,
DEVICE_CLS_POWER_FACTOR,
DEVICE_CLS_REACTIVE_POWER
DEVICE_CLS_REACTIVE_POWER,
DEVICE_CLS_CONNECTIVITY,
DEVICE_CLS_DURATION,
DEVICE_CLS_SIGNAL_STRENGTH,
DEVICE_CLS_TEMPERATURE,
DEVICE_CLS_RESTART
};
const char* const deviceClasses[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power" };
enum {
const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" };
enum StateClassType {
STATE_CLS_NONE = 0,
STATE_CLS_MEASUREMENT,
STATE_CLS_TOTAL_INCREASING
};
const char* const stateClasses[] = { 0, "measurement", "total_increasing" };
const char* const stateClass_name[] = { 0, "measurement", "total_increasing" };
enum CategoryType {
CATEGORY_NONE = 0,
CATEGORY_CONFIG,
CATEGORY_DIAGNOSTIC
};
const char* const category_name[] = { 0, "config", "diagnostic" };
typedef struct {
FieldId_t fieldId; // field id
uint8_t deviceClsId; // device class
uint8_t stateClsId; // state class
DeviceClassType deviceClsId; // device class
StateClassType stateClsId; // state class
} byteAssign_fieldDeviceClass_t;
const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
@ -58,13 +72,24 @@ public:
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic);
void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = "");
void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload);
void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100, float step = 1.0);
void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off);
static void publish(const String& subtopic, const String& payload);
static void publish(const String& subtopic, const JsonDocument& doc);
static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category);
// Binary Sensor
static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category);
static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category);
static void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category);
// Sensor
static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category);
static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category);
static void publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category);
static void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
static void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const CategoryType category);
static void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const CategoryType category);
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(JsonDocument& doc);

View File

@ -5,6 +5,8 @@
#include <Hoymiles.h>
#include <TaskSchedulerDeclarations.h>
#include <espMqttClient.h>
#include <frozen/map.h>
#include <frozen/string.h>
class MqttHandleInverterClass {
public:
@ -19,7 +21,6 @@ public:
private:
void loop();
void publishField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId);
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total);
Task _loopTask;
@ -41,6 +42,29 @@ private:
FLD_IRR,
FLD_Q
};
enum class Topic : unsigned {
LimitPersistentRelative,
LimitPersistentAbsolute,
LimitNonPersistentRelative,
LimitNonPersistentAbsolute,
Power,
Restart,
ResetRfStats,
};
static constexpr frozen::string _cmdtopic = "+/cmd/";
static constexpr frozen::map<frozen::string, Topic, 7> _subscriptions = {
{ "limit_persistent_relative", Topic::LimitPersistentRelative },
{ "limit_persistent_absolute", Topic::LimitPersistentAbsolute },
{ "limit_nonpersistent_relative", Topic::LimitNonPersistentRelative },
{ "limit_nonpersistent_absolute", Topic::LimitNonPersistentAbsolute },
{ "power", Topic::Power },
{ "restart", Topic::Restart },
{ "reset_rf_stats", Topic::ResetRfStats },
};
void onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total);
};
extern MqttHandleInverterClass MqttHandleInverter;

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "W5500.h"
#include <DNSServer.h>
#include <TaskSchedulerDeclarations.h>
#include <WiFi.h>
@ -23,10 +24,10 @@ enum class network_event {
NETWORK_EVENT_MAX
};
typedef std::function<void(network_event event)> NetworkEventCb;
typedef std::function<void(network_event event)> DtuNetworkEventCb;
typedef struct NetworkEventCbList {
NetworkEventCb cb;
DtuNetworkEventCb cb;
network_event event;
NetworkEventCbList()
@ -53,7 +54,7 @@ public:
bool isConnected() const;
network_mode NetworkMode() const;
bool onEvent(NetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX);
bool onEvent(DtuNetworkEventCb cbEvent, const network_event event = network_event::NETWORK_EVENT_MAX);
void raiseEvent(const network_event event);
private:
@ -62,7 +63,7 @@ private:
void setStaticIp();
void handleMDNS();
void setupMode();
void NetworkEvent(const WiFiEvent_t event);
void NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info);
Task _loopTask;
@ -83,6 +84,7 @@ private:
bool _ethConnected = false;
std::vector<NetworkEventCbList_t> _cbEventList;
bool _lastMdnsEnabled = false;
std::unique_ptr<W5500> _w5500;
};
extern NetworkSettingsClass NetworkSettings;
extern NetworkSettingsClass NetworkSettings;

View File

@ -26,6 +26,13 @@ struct PinMapping_t {
int8_t cmt_gpio3;
int8_t cmt_sdio;
int8_t w5500_mosi;
int8_t w5500_miso;
int8_t w5500_sclk;
int8_t w5500_cs;
int8_t w5500_int;
int8_t w5500_rst;
int8_t eth_phy_addr;
bool eth_enabled;
int eth_power;
@ -49,10 +56,11 @@ public:
bool isValidNrf24Config() const;
bool isValidCmt2300Config() const;
bool isValidW5500Config() const;
bool isValidEthConfig() const;
private:
PinMapping_t _pinMapping;
};
extern PinMappingClass PinMapping;
extern PinMappingClass PinMapping;

18
include/RestartHelper.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
class RestartHelperClass {
public:
RestartHelperClass();
void init(Scheduler& scheduler);
void triggerRestart();
private:
void loop();
Task _rebootTask;
};
extern RestartHelperClass RestartHelper;

View File

@ -9,7 +9,6 @@ public:
static uint32_t getChipId();
static uint64_t generateDtuSerial();
static int getTimezoneOffset();
static void restartDtu();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles();
};

20
include/W5500.h Normal file
View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <esp_eth.h> // required for esp_eth_handle_t
#include <esp_netif.h>
class W5500 {
public:
explicit W5500(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst);
W5500(const W5500&) = delete;
W5500& operator=(const W5500&) = delete;
~W5500();
String macAddress();
private:
esp_eth_handle_t eth_handle;
esp_netif_t* eth_netif;
};

View File

@ -32,6 +32,7 @@ enum WebApiError {
InverterChanged,
InverterDeleted,
InverterOrdered,
InverterStatsResetted,
LimitBase = 5000,
LimitSerialZero,

View File

@ -14,4 +14,5 @@ private:
void onInverterEdit(AsyncWebServerRequest* request);
void onInverterDelete(AsyncWebServerRequest* request);
void onInverterOrder(AsyncWebServerRequest* request);
void onInverterStatReset(AsyncWebServerRequest* request);
};

View File

@ -5,4 +5,5 @@
extern const char *__COMPILED_GIT_HASH__;
extern const char *__COMPILED_GIT_BRANCH__;
// extern const char *__COMPILED_DATE_TIME_UTC_STR__;

View File

@ -1,142 +0,0 @@
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_rom_gpio.h> // for esp_rom_gpio_connect_out_signal
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
// for ESP32 this is the so-called HSPI
// for ESP32-S2/S3/C3 this nomenclature does not really exist anymore,
// it is simply the first externally usable hardware SPI master controller
#define SPI_CMT SPI2_HOST
spi_device_handle_t spi_reg, spi_fifo;
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
spi_bus_config_t buscfg = {
.mosi_io_num = pin_sdio,
.miso_io_num = -1, // single wire MOSI/MISO
.sclk_io_num = pin_clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 32,
};
spi_device_interface_config_t devcfg = {
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 1,
.cs_ena_posttrans = 1,
.clock_speed_hz = spi_speed,
.spics_io_num = pin_cs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_CMT, &buscfg, SPI_DMA_DISABLED));
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg, &spi_reg));
// FiFo
spi_device_interface_config_t devcfg2 = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.cs_ena_pretrans = 2,
.cs_ena_posttrans = (uint8_t)(1 / (spi_speed * 10e6 * 2) + 2), // >2 us
.clock_speed_hz = spi_speed,
.spics_io_num = pin_fcs,
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = NULL,
.post_cb = NULL,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_CMT, &devcfg2, &spi_fifo));
esp_rom_gpio_connect_out_signal(pin_sdio, spi_periph_signal[SPI_CMT].spid_out, true, false);
delay(100);
}
void cmt_spi3_write(const uint8_t addr, const uint8_t dat)
{
uint8_t tx_data;
tx_data = ~dat;
spi_transaction_t t = {
.cmd = 1,
.addr = ~addr,
.length = 8,
.tx_buffer = &tx_data,
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
}
uint8_t cmt_spi3_read(const uint8_t addr)
{
uint8_t rx_data;
spi_transaction_t t = {
.cmd = 0,
.addr = ~addr,
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_reg, &t));
SPI_PARAM_UNLOCK();
delayMicroseconds(100);
return rx_data;
}
void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len)
{
uint8_t tx_data;
spi_transaction_t t = {
.length = 8,
.tx_buffer = &tx_data, // reference to write data
.rx_buffer = NULL
};
SPI_PARAM_LOCK();
for (uint8_t i = 0; i < len; i++) {
tx_data = ~buf[i]; // negate buffer contents
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
}
SPI_PARAM_UNLOCK();
}
void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len)
{
uint8_t rx_data;
spi_transaction_t t = {
.length = 8,
.rxlength = 8,
.tx_buffer = NULL,
.rx_buffer = &rx_data
};
SPI_PARAM_LOCK();
for (uint8_t i = 0; i < len; i++) {
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_fifo, &t));
delayMicroseconds(4); // > 4 us
buf[i] = rx_data;
}
SPI_PARAM_UNLOCK();
}

155
lib/CMT2300a/cmt_spi3.cpp Normal file
View File

@ -0,0 +1,155 @@
#include "cmt_spi3.h"
#include <Arduino.h>
#include <driver/spi_master.h>
#include <SpiManager.h>
SemaphoreHandle_t paramLock = NULL;
#define SPI_PARAM_LOCK() \
do { \
} while (xSemaphoreTake(paramLock, portMAX_DELAY) != pdPASS)
#define SPI_PARAM_UNLOCK() xSemaphoreGive(paramLock)
static void IRAM_ATTR pre_cb(spi_transaction_t *trans) {
gpio_set_level(*reinterpret_cast<gpio_num_t*>(trans->user), 0);
}
static void IRAM_ATTR post_cb(spi_transaction_t *trans) {
gpio_set_level(*reinterpret_cast<gpio_num_t*>(trans->user), 1);
}
spi_device_handle_t spi;
gpio_num_t cs_reg, cs_fifo;
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed)
{
paramLock = xSemaphoreCreateMutex();
auto bus_config = std::make_shared<SpiBusConfig>(
static_cast<gpio_num_t>(pin_sdio),
GPIO_NUM_NC,
static_cast<gpio_num_t>(pin_clk)
);
spi_device_interface_config_t device_config {
.command_bits = 0, // set by transactions individually
.address_bits = 0, // set by transactions individually
.dummy_bits = 0,
.mode = 0, // SPI mode 0
.duty_cycle_pos = 0,
.cs_ena_pretrans = 2, // only 1 pre and post cycle would be required for register access
.cs_ena_posttrans = static_cast<uint8_t>(2 * spi_speed / 1000000), // >2 us
.clock_speed_hz = spi_speed,
.input_delay_ns = 0,
.spics_io_num = -1, // CS handled by callbacks
.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE,
.queue_size = 1,
.pre_cb = pre_cb,
.post_cb = post_cb,
};
spi = SpiManagerInst.alloc_device("", bus_config, device_config);
if (!spi)
ESP_ERROR_CHECK(ESP_FAIL);
cs_reg = static_cast<gpio_num_t>(pin_cs);
ESP_ERROR_CHECK(gpio_reset_pin(cs_reg));
ESP_ERROR_CHECK(gpio_set_level(cs_reg, 1));
ESP_ERROR_CHECK(gpio_set_direction(cs_reg, GPIO_MODE_OUTPUT));
cs_fifo = static_cast<gpio_num_t>(pin_fcs);
ESP_ERROR_CHECK(gpio_reset_pin(cs_fifo));
ESP_ERROR_CHECK(gpio_set_level(cs_fifo, 1));
ESP_ERROR_CHECK(gpio_set_direction(cs_fifo, GPIO_MODE_OUTPUT));
}
void cmt_spi3_write(const uint8_t addr, const uint8_t data)
{
spi_transaction_ext_t trans {
.base {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = 0,
.addr = addr,
.length = 8,
.rxlength = 0,
.user = &cs_reg, // CS for register access
.tx_buffer = &data,
.rx_buffer = nullptr,
},
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast<spi_transaction_t*>(&trans)));
SPI_PARAM_UNLOCK();
}
uint8_t cmt_spi3_read(const uint8_t addr)
{
uint8_t data;
spi_transaction_ext_t trans {
.base {
.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR,
.cmd = 1,
.addr = addr,
.length = 0,
.rxlength = 8,
.user = &cs_reg, // CS for register access
.tx_buffer = nullptr,
.rx_buffer = &data,
},
.command_bits = 1,
.address_bits = 7,
.dummy_bits = 0,
};
SPI_PARAM_LOCK();
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, reinterpret_cast<spi_transaction_t*>(&trans)));
SPI_PARAM_UNLOCK();
return data;
}
void cmt_spi3_write_fifo(const uint8_t* buf, const uint16_t len)
{
spi_transaction_t trans {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 8,
.rxlength = 0,
.user = &cs_fifo, // CS for FIFO access
.tx_buffer = nullptr,
.rx_buffer = nullptr,
};
SPI_PARAM_LOCK();
spi_device_acquire_bus(spi, portMAX_DELAY);
for (uint8_t i = 0; i < len; i++) {
trans.tx_buffer = buf + i;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
}
spi_device_release_bus(spi);
SPI_PARAM_UNLOCK();
}
void cmt_spi3_read_fifo(uint8_t* buf, const uint16_t len)
{
spi_transaction_t trans {
.flags = 0,
.cmd = 0,
.addr = 0,
.length = 0,
.rxlength = 8,
.user = &cs_fifo, // CS for FIFO access
.tx_buffer = nullptr,
.rx_buffer = nullptr,
};
SPI_PARAM_LOCK();
spi_device_acquire_bus(spi, portMAX_DELAY);
for (uint8_t i = 0; i < len; i++) {
trans.rx_buffer = buf + i;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
}
spi_device_release_bus(spi);
SPI_PARAM_UNLOCK();
}

View File

@ -3,7 +3,11 @@
#include <stdint.h>
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const uint32_t spi_speed);
#ifdef __cplusplus
extern "C" {
#endif
void cmt_spi3_init(const int8_t pin_sdio, const int8_t pin_clk, const int8_t pin_cs, const int8_t pin_fcs, const int32_t spi_speed);
void cmt_spi3_write(const uint8_t addr, const uint8_t dat);
uint8_t cmt_spi3_read(const uint8_t addr);
@ -11,4 +15,8 @@ uint8_t cmt_spi3_read(const uint8_t addr);
void cmt_spi3_write_fifo(const uint8_t* p_buf, const uint16_t len);
void cmt_spi3_read_fifo(uint8_t* p_buf, const uint16_t len);
#ifdef __cplusplus
}
#endif
#endif

View File

@ -4,6 +4,7 @@
*/
#include "Hoymiles.h"
#include "Utils.h"
#include "inverters/HERF_1CH.h"
#include "inverters/HERF_2CH.h"
#include "inverters/HERF_4CH.h"
#include "inverters/HMS_1CH.h"
@ -135,15 +136,7 @@ void HoymilesClass::loop()
if (currentWeekDay != lastWeekDay) {
for (auto& inv : _inverters) {
// Have to reset the offets first, otherwise it will
// Substract the offset from zero which leads to a high value
inv->Statistics()->resetYieldDayCorrection();
if (inv->getZeroYieldDayOnMidnight()) {
inv->Statistics()->zeroDailyData();
}
if (inv->getClearEventlogOnMidnight()) {
inv->EventLog()->clearBuffer();
}
inv->performDailyTask();
}
lastWeekDay = currentWeekDay;
@ -173,6 +166,8 @@ std::shared_ptr<InverterAbstract> HoymilesClass::addInverter(const char* name, c
i = std::make_shared<HM_2CH>(_radioNrf.get(), serial);
} else if (HM_1CH::isValidSerial(serial)) {
i = std::make_shared<HM_1CH>(_radioNrf.get(), serial);
} else if (HERF_1CH::isValidSerial(serial)) {
i = std::make_shared<HERF_1CH>(_radioNrf.get(), serial);
} else if (HERF_2CH::isValidSerial(serial)) {
i = std::make_shared<HERF_2CH>(_radioNrf.get(), serial);
} else if (HERF_4CH::isValidSerial(serial)) {
@ -200,9 +195,9 @@ std::shared_ptr<InverterAbstract> HoymilesClass::getInverterByPos(const uint8_t
std::shared_ptr<InverterAbstract> HoymilesClass::getInverterBySerial(const uint64_t serial)
{
for (uint8_t i = 0; i < _inverters.size(); i++) {
if (_inverters[i]->serial() == serial) {
return _inverters[i];
for (auto& inv : _inverters) {
if (inv->serial() == serial) {
return inv;
}
}
return nullptr;
@ -214,9 +209,7 @@ std::shared_ptr<InverterAbstract> HoymilesClass::getInverterByFragment(const fra
return nullptr;
}
std::shared_ptr<InverterAbstract> inv;
for (uint8_t i = 0; i < _inverters.size(); i++) {
inv = _inverters[i];
for (auto& inv : _inverters) {
serial_u p;
p.u64 = inv->serial();

View File

@ -66,16 +66,31 @@ void HoymilesRadio::handleReceivedPackage()
} else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded");
// Statistics: Count RX Fail No Answer
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailNoAnswer++;
}
_commandQueue.pop();
_busyFlag = false;
} else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) {
Hoymiles.getMessageOutput()->println("Retransmit timeout");
// Statistics: Count RX Fail Partial Answer
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailPartialAnswer++;
}
_commandQueue.pop();
_busyFlag = false;
} else if (verifyResult == FRAGMENT_HANDLE_ERROR) {
Hoymiles.getMessageOutput()->println("Packet handling error");
// Statistics: Count RX Fail Corrupt Data
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxFailCorruptData++;
}
_commandQueue.pop();
_busyFlag = false;
@ -83,17 +98,26 @@ void HoymilesRadio::handleReceivedPackage()
// Perform Retransmit
Hoymiles.getMessageOutput()->print("Request retransmit: ");
Hoymiles.getMessageOutput()->println(verifyResult);
// Statistics: Count TX Re-Request Fragment
inv->RadioStats.TxReRequestFragment++;
sendRetransmitPacket(verifyResult);
} else {
// Successful received all packages
Hoymiles.getMessageOutput()->println("Success");
// Statistics: Count RX Success
if (inv->RadioStats.TxRequestData > 0) {
inv->RadioStats.RxSuccess++;
}
_commandQueue.pop();
_busyFlag = false;
}
} else {
// If inverter was not found, assume the command is invalid
Hoymiles.getMessageOutput()->println("RX: Invalid inverter found");
// Statistics: Count RX Fail Unknown Data
_commandQueue.pop();
_busyFlag = false;
}
@ -105,6 +129,9 @@ void HoymilesRadio::handleReceivedPackage()
auto inv = Hoymiles.getInverterBySerial(cmd->getTargetAddress());
if (nullptr != inv) {
inv->clearRxFragmentBuffer();
// Statistics: TX Requests
inv->RadioStats.TxRequestData++;
sendEsbPacket(*cmd);
} else {
Hoymiles.getMessageOutput()->println("TX: Invalid inverter found");

View File

@ -0,0 +1,55 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "HERF_1CH.h"
static const byteAssign_t byteAssignment[] = {
{ TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 },
{ TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 },
{ TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 },
{ TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 },
{ TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 },
{ TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_Q, UNIT_VAR, 40, 2, 10, false, 1 }, // to be verified
{ TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 },
{ TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, // to be verified
{ TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, // to be verified
{ TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 },
{ TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 },
{ TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 },
{ TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 }
};
HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial)
: HM_Abstract(radio, serial) {};
bool HERF_1CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x284100000000 && serial <= 0x2841ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x2841;
}
String HERF_1CH::typeName() const
{
return "HERF-300-1T";
}
const byteAssign_t* HERF_1CH::getByteAssignment() const
{
return byteAssignment;
}
uint8_t HERF_1CH::getByteAssignmentSize() const
{
return sizeof(byteAssignment) / sizeof(byteAssignment[0]);
}

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "HM_Abstract.h"
class HERF_1CH : public HM_Abstract {
public:
explicit HERF_1CH(HoymilesRadio* radio, const uint64_t serial);
static bool isValidSerial(const uint64_t serial);
String typeName() const;
const byteAssign_t* getByteAssignment() const;
uint8_t getByteAssignmentSize() const;
};

View File

@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial)
{
// serial >= 0x114400000000 && serial <= 0x1144ffffffff
uint16_t preSerial = (serial >> 32) & 0xffff;
return preSerial == 0x1144 || preSerial == 0x1143;
return preSerial == 0x1144 || preSerial == 0x1143 || preSerial == 0x1410;
}
String HMS_2CH::typeName() const

View File

@ -272,3 +272,22 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd)
return FRAGMENT_OK;
}
void InverterAbstract::performDailyTask()
{
// Have to reset the offets first, otherwise it will
// Substract the offset from zero which leads to a high value
Statistics()->resetYieldDayCorrection();
if (getZeroYieldDayOnMidnight()) {
Statistics()->zeroDailyData();
}
if (getClearEventlogOnMidnight()) {
EventLog()->clearBuffer();
}
resetRadioStats();
}
void InverterAbstract::resetRadioStats()
{
RadioStats = {};
}

View File

@ -65,6 +65,30 @@ public:
void addRxFragment(const uint8_t fragment[], const uint8_t len);
uint8_t verifyAllFragments(CommandAbstract& cmd);
void performDailyTask();
void resetRadioStats();
struct {
// TX Request Data
uint32_t TxRequestData;
// TX Re-Request Fragment
uint32_t TxReRequestFragment;
// RX Success
uint32_t RxSuccess;
// RX Fail Partial Answer
uint32_t RxFailPartialAnswer;
// RX Fail No Answer
uint32_t RxFailNoAnswer;
// RX Fail Corrupt Data
uint32_t RxFailCorruptData;
} RadioStats = {};
virtual bool sendStatsRequest() = 0;
virtual bool sendAlarmLogRequest(const bool force = false) = 0;
virtual bool sendDevInfoRequest() = 0;

View File

@ -1,15 +1,16 @@
# Class overview
| Class | Models | Serial range |
| --------------| --------------------------- | ------------ |
| HM_1CH | HM-300/350/400-1T | 1121 |
| HM_2CH | HM-600/700/800-2T | 1141 |
| HM_4CH | HM-1000/1200/1500-4T | 1161 |
| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 |
| HMS_1CHv2 | HMS-500-1T v2 | 1125 |
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 |
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 |
| HERF_2CH | HERF 800 | 2821 |
| HERF_4CH | HERF 1800 | 2801 |
| Class | Models | Serial range |
| --------------| --------------------------- | ------------- -- |
| HM_1CH | HM-300/350/400-1T | 1121 |
| HM_2CH | HM-600/700/800-2T | 1141 |
| HM_4CH | HM-1000/1200/1500-4T | 1161 |
| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 |
| HMS_1CHv2 | HMS-500-1T v2 | 1125 |
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 |
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
| HMT_6CH | HMT-1800/2250-6T | 1382 |
| HERF_1CH | HERF 300 | 2841 |
| HERF_2CH | HERF 800 | 2821 |
| HERF_4CH | HERF 1800 | 2801 |

View File

@ -0,0 +1,13 @@
{
"name": "SpiManager",
"keywords": "spi",
"description": "Library for managing the allocation of dedicated or shared SPI buses on the ESP32.",
"authors": {
"name": "Lennart Ferlemann"
},
"version": "0.0.1",
"frameworks": "arduino",
"platforms": [
"espressif32"
]
}

View File

@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiBus.h"
#include "SpiBusConfig.h"
#include "SpiCallback.h"
SpiBus::SpiBus(const std::string& _id, spi_host_device_t _host_device)
: id(_id)
, host_device(_host_device)
, cur_config(nullptr)
{
spi_bus_config_t bus_config {
.mosi_io_num = -1,
.miso_io_num = -1,
.sclk_io_num = -1,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = SPI_MAX_DMA_LEN,
.flags = 0,
.intr_flags = 0
};
ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, SPI_DMA_CH_AUTO));
}
SpiBus::~SpiBus()
{
ESP_ERROR_CHECK(spi_bus_free(host_device));
}
spi_device_handle_t SpiBus::add_device(const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
if (!SpiCallback::patch(shared_from_this(), bus_config, device_config))
return nullptr;
spi_device_handle_t device;
ESP_ERROR_CHECK(spi_bus_add_device(host_device, &device_config, &device));
return device;
}
// TODO: add remove_device (with spi_device_acquire_bus)
void SpiBus::apply_config(SpiBusConfig* config)
{
if (cur_config)
cur_config->unpatch(host_device);
cur_config = config;
if (cur_config)
cur_config->patch(host_device);
}

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <driver/spi_master.h>
#include <memory>
#include <string>
class SpiBusConfig;
class SpiBus : public std::enable_shared_from_this<SpiBus> {
public:
explicit SpiBus(const std::string& id, spi_host_device_t host_device);
SpiBus(const SpiBus&) = delete;
SpiBus& operator=(const SpiBus&) = delete;
~SpiBus();
inline __attribute__((always_inline)) void require_config(SpiBusConfig* config)
{
if (config == cur_config)
return;
apply_config(config);
}
inline __attribute__((always_inline)) void free_config(SpiBusConfig* config)
{
if (config != cur_config)
return;
apply_config(nullptr);
}
inline const std::string& get_id() const
{
return id;
}
inline spi_host_device_t get_host_device() const
{
return host_device;
}
spi_device_handle_t add_device(const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
private:
void apply_config(SpiBusConfig* config);
std::string id;
spi_host_device_t host_device;
SpiBusConfig* cur_config;
};

View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiBusConfig.h"
#include <driver/gpio.h>
#include <esp_rom_gpio.h>
#include <soc/spi_periph.h>
SpiBusConfig::SpiBusConfig(gpio_num_t _pin_mosi, gpio_num_t _pin_miso, gpio_num_t _pin_sclk)
: pin_mosi(_pin_mosi)
, pin_miso(_pin_miso)
, pin_sclk(_pin_sclk)
{
if (pin_mosi != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi));
ESP_ERROR_CHECK(gpio_set_direction(pin_mosi, GPIO_MODE_INPUT_OUTPUT));
}
if (pin_miso != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_miso));
ESP_ERROR_CHECK(gpio_set_direction(pin_miso, GPIO_MODE_INPUT));
}
if (pin_sclk != GPIO_NUM_NC) {
ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk));
ESP_ERROR_CHECK(gpio_set_direction(pin_sclk, GPIO_MODE_INPUT_OUTPUT));
}
}
SpiBusConfig::~SpiBusConfig()
{
if (pin_mosi != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_mosi));
if (pin_miso != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_miso));
if (pin_sclk != GPIO_NUM_NC)
ESP_ERROR_CHECK(gpio_reset_pin(pin_sclk));
}
void SpiBusConfig::patch(spi_host_device_t host_device)
{
if (pin_mosi != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_mosi, spi_periph_signal[host_device].spid_out, false, false);
esp_rom_gpio_connect_in_signal(pin_mosi, spi_periph_signal[host_device].spid_in, false);
}
if (pin_miso != GPIO_NUM_NC)
esp_rom_gpio_connect_in_signal(pin_miso, spi_periph_signal[host_device].spiq_in, false);
if (pin_sclk != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_sclk, spi_periph_signal[host_device].spiclk_out, false, false);
esp_rom_gpio_connect_in_signal(pin_sclk, spi_periph_signal[host_device].spiclk_in, false);
}
}
void SpiBusConfig::unpatch(spi_host_device_t host_device)
{
if (pin_mosi != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_mosi, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spid_in, false);
}
if (pin_miso != GPIO_NUM_NC)
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiq_in, false);
if (pin_sclk != GPIO_NUM_NC) {
esp_rom_gpio_connect_out_signal(pin_sclk, SIG_GPIO_OUT_IDX, false, false);
esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, spi_periph_signal[host_device].spiclk_in, false);
}
}

View File

@ -0,0 +1,21 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <hal/gpio_types.h>
#include <hal/spi_types.h>
class SpiBusConfig {
public:
explicit SpiBusConfig(gpio_num_t pin_mosi, gpio_num_t pin_miso, gpio_num_t pin_sclk);
SpiBusConfig(const SpiBusConfig&) = delete;
SpiBusConfig& operator=(const SpiBusConfig&) = delete;
~SpiBusConfig();
void patch(spi_host_device_t host_device);
void unpatch(spi_host_device_t host_device);
private:
gpio_num_t pin_mosi;
gpio_num_t pin_miso;
gpio_num_t pin_sclk;
};

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiCallback.h"
#include "SpiBus.h"
#include <array>
#include <optional>
namespace SpiCallback {
namespace {
struct CallbackData {
std::shared_ptr<SpiBus> bus;
std::shared_ptr<SpiBusConfig> config;
transaction_cb_t inner_pre_cb;
transaction_cb_t inner_post_cb;
};
std::array<std::optional<CallbackData>, SPI_MANAGER_CALLBACK_COUNT> instances;
template <int N>
void IRAM_ATTR fn_pre_cb(spi_transaction_t* trans)
{
instances[N]->bus->require_config(instances[N]->config.get());
if (instances[N]->inner_pre_cb)
instances[N]->inner_pre_cb(trans);
}
template <int N>
void IRAM_ATTR fn_post_cb(spi_transaction_t* trans)
{
if (instances[N]->inner_post_cb)
instances[N]->inner_post_cb(trans);
}
template <int N>
inline __attribute__((always_inline)) bool alloc(CallbackData*& instance, transaction_cb_t& pre_cb, transaction_cb_t& post_cb)
{
if constexpr (N > 0) {
if (alloc<N - 1>(instance, pre_cb, post_cb))
return true;
if (!instances[N - 1]) {
instances[N - 1].emplace();
instance = &*instances[N - 1];
pre_cb = fn_pre_cb<N - 1>;
post_cb = fn_post_cb<N - 1>;
return true;
}
}
return false;
}
}
bool patch(const std::shared_ptr<SpiBus>& bus, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
CallbackData* instance;
transaction_cb_t pre_cb;
transaction_cb_t post_cb;
if (!alloc<SPI_MANAGER_CALLBACK_COUNT>(instance, pre_cb, post_cb))
return false;
instance->bus = bus;
instance->config = bus_config;
instance->inner_pre_cb = device_config.pre_cb;
instance->inner_post_cb = device_config.post_cb;
device_config.pre_cb = pre_cb;
device_config.post_cb = post_cb;
return true;
}
}

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <driver/spi_master.h>
#include <memory>
// Pre and post callbacks for 2 buses with 3 devices each
#define SPI_MANAGER_CALLBACK_COUNT 6
class SpiBus;
class SpiBusConfig;
namespace SpiCallback {
bool patch(const std::shared_ptr<SpiBus>& bus, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
}

View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SpiManager.h"
#ifdef ARDUINO
#include <SPI.h>
#endif
SpiManager::SpiManager()
{
}
#ifdef ARDUINO
std::optional<uint8_t> SpiManager::to_arduino(spi_host_device_t host_device)
{
switch (host_device) {
#if CONFIG_IDF_TARGET_ESP32
case SPI1_HOST:
return FSPI;
case SPI2_HOST:
return HSPI;
case SPI3_HOST:
return VSPI;
#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
case SPI2_HOST:
return FSPI;
case SPI3_HOST:
return HSPI;
#elif CONFIG_IDF_TARGET_ESP32C3
case SPI2_HOST:
return FSPI;
#endif
default:
return std::nullopt;
}
}
#endif
bool SpiManager::register_bus(spi_host_device_t host_device)
{
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (available_buses[i])
continue;
available_buses[i] = host_device;
return true;
}
return false;
}
bool SpiManager::claim_bus(spi_host_device_t& host_device)
{
for (int i = SPI_MANAGER_NUM_BUSES - 1; i >= 0; --i) {
if (!available_buses[i])
continue;
host_device = *available_buses[i];
available_buses[i].reset();
return true;
}
return false;
}
#ifdef ARDUINO
std::optional<uint8_t> SpiManager::claim_bus_arduino()
{
spi_host_device_t host_device;
if (!claim_bus(host_device))
return std::nullopt;
return to_arduino(host_device);
}
#endif
spi_device_handle_t SpiManager::alloc_device(const std::string& bus_id, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config)
{
std::shared_ptr<SpiBus> shared_bus = get_shared_bus(bus_id);
if (!shared_bus)
return nullptr;
return shared_bus->add_device(bus_config, device_config);
}
std::shared_ptr<SpiBus> SpiManager::get_shared_bus(const std::string& bus_id)
{
// look for existing shared bus
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (!shared_buses[i])
continue;
if (shared_buses[i]->get_id() == bus_id)
return shared_buses[i];
}
// create new shared bus
for (int i = 0; i < SPI_MANAGER_NUM_BUSES; ++i) {
if (shared_buses[i])
continue;
spi_host_device_t host_device;
if (!claim_bus(host_device))
return nullptr;
shared_buses[i] = std::make_shared<SpiBus>(bus_id, host_device);
return shared_buses[i];
}
return nullptr;
}
SpiManager SpiManagerInst;

View File

@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "SpiBus.h"
#include "SpiBusConfig.h"
#include <driver/spi_master.h>
#include <array>
#include <memory>
#include <optional>
#include <utility>
#define SPI_MANAGER_NUM_BUSES SOC_SPI_PERIPH_NUM
class SpiManager {
public:
explicit SpiManager();
SpiManager(const SpiManager&) = delete;
SpiManager& operator=(const SpiManager&) = delete;
#ifdef ARDUINO
static std::optional<uint8_t> to_arduino(spi_host_device_t host_device);
#endif
bool register_bus(spi_host_device_t host_device);
bool claim_bus(spi_host_device_t& host_device);
#ifdef ARDUINO
std::optional<uint8_t> claim_bus_arduino();
#endif
spi_device_handle_t alloc_device(const std::string& bus_id, const std::shared_ptr<SpiBusConfig>& bus_config, spi_device_interface_config_t& device_config);
private:
std::shared_ptr<SpiBus> get_shared_bus(const std::string& bus_id);
std::array<std::optional<spi_host_device_t>, SPI_MANAGER_NUM_BUSES> available_buses;
std::array<std::shared_ptr<SpiBus>, SPI_MANAGER_NUM_BUSES> shared_buses;
};
extern SpiManager SpiManagerInst;

View File

@ -36,9 +36,20 @@ def get_build_version():
return build_version
def get_build_branch():
try:
branch_name = porcelain.active_branch('.').decode('utf-8') # '.' refers to the repository root dir
except Exception as err:
branch_name = "master"
print("Firmware Branch: " + branch_name)
return branch_name
def get_firmware_specifier_build_flag():
build_version = get_build_version()
build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\""
build_branch = get_build_branch()
build_flag += " -D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\""
return (build_flag)
@ -64,6 +75,8 @@ def do_main():
if 1:
# Add the description of the current git revision
lines += 'const char *__COMPILED_GIT_HASH__ = "%s";\n' % (get_build_version())
# ... and git branch
lines += 'const char *__COMPILED_GIT_BRANCH__ = "%s";\n' % (get_build_branch())
updateFileIfChanged(targetfile, bytes(lines, "utf-8"))

View File

@ -18,20 +18,64 @@
Import("env")
env = DefaultEnvironment()
platform = env.PioPlatform()
import sys
from os.path import join, getsize
import csv
import subprocess
import shutil
from os.path import join, getsize, exists, isdir
from os import listdir
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool
def esp32_build_filesystem(fs_name, fs_size):
filesystem_dir = env.subst("$PROJECT_DATA_DIR")
print("Creating %dKiB filesystem with content:" % (int(fs_size, 0)/1024) )
if not isdir(filesystem_dir) or not listdir(filesystem_dir):
print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!")
return False
# this does not work on GitHub, results in 'mklittlefs: No such file or directory'
tool = shutil.which(env.subst(env["MKFSTOOL"]))
if tool is None or not exists(tool):
print("Using fallback mklittlefs")
tool = "~/.platformio/packages/tool-mklittlefs/mklittlefs"
cmd = (tool, "-c", filesystem_dir, "-s", fs_size, fs_name)
returncode = subprocess.call(cmd, shell=False)
print("Return Code:", returncode)
return True
def esp32_create_combined_bin(source, target, env):
print("Generating combined binary for serial flashing")
# The offset from begin of the file where the app0 partition starts
# This is defined in the partition .csv file
app_offset = 0x10000
fs_offset = -1
fs_name = env.subst("$BUILD_DIR/littlefs.bin")
with open(env.BoardConfig().get("build.partitions")) as csv_file:
print("Read partitions from ", env.BoardConfig().get("build.partitions"))
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
for row in csv_reader:
if line_count == 0:
print(f'{", ".join(row)}')
line_count += 1
else:
if (len(row) < 4):
continue
print(f'{row[0]} {row[1]} {row[2]} {row[3]} {row[4]}')
line_count += 1
if(row[0] == 'app0'):
app_offset = int(row[3], base=16)
elif(row[0] == 'spiffs'):
partition_size = row[4]
if esp32_build_filesystem(fs_name, partition_size):
fs_offset = int(row[3], base=16)
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
@ -77,9 +121,13 @@ def esp32_create_combined_bin(source, target, env):
print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
if fs_offset != -1:
print(f" - {hex(fs_offset)} | {fs_name}")
cmd += [hex(fs_offset), fs_name]
print('Using esptool.py arguments: %s' % ' '.join(cmd))
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)

View File

@ -19,7 +19,9 @@ extra_configs =
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
framework = arduino
platform = espressif32@6.8.1
platform = espressif32@6.9.0
platform_packages =
platformio/tool-mklittlefs
build_flags =
-DPIOENV=\"$PIOENV\"
@ -39,13 +41,13 @@ build_unflags =
-std=gnu++11
lib_deps =
mathieucarbou/ESPAsyncWebServer @ 3.1.2
bblanchon/ArduinoJson @ 7.1.0
mathieucarbou/ESPAsyncWebServer @ 3.3.1
bblanchon/ArduinoJson @ 7.2.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.9
olikraus/U8g2 @ 2.35.19
olikraus/U8g2 @ 2.35.30
buelowp/sunset @ 1.1.7
https://github.com/arkhipenko/TaskScheduler#testing
arkhipenko/TaskScheduler @ 3.8.5
extra_scripts =
pre:pio-scripts/auto_firmware_version.py
@ -227,6 +229,7 @@ build_flags = ${env.build_flags}
-DLED0=17
-DLED1=18
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
[env:opendtufusionv2]
board = esp32-s3-devkitc-1
@ -250,3 +253,32 @@ build_flags = ${env.build_flags}
-DCMT_SDIO=5
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
[env:opendtufusionv2_shield]
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
debug_tool = esp-builtin
debug_speed = 12000
build_flags = ${env.build_flags}
-DHOYMILES_PIN_MISO=48
-DHOYMILES_PIN_MOSI=35
-DHOYMILES_PIN_SCLK=36
-DHOYMILES_PIN_IRQ=47
-DHOYMILES_PIN_CE=38
-DHOYMILES_PIN_CS=37
-DLED0=17
-DLED1=18
-DCMT_CLK=6
-DCMT_CS=4
-DCMT_FCS=21
-DCMT_GPIO2=3
-DCMT_GPIO3=8
-DCMT_SDIO=5
-DW5500_MOSI=40
-DW5500_MISO=41
-DW5500_SCLK=39
-DW5500_CS=42
-DW5500_INT=44
-DW5500_RST=43
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1

View File

@ -8,20 +8,7 @@
#include "PinMapping.h"
#include "SunPosition.h"
#include <Hoymiles.h>
// the NRF shall use the second externally usable HW SPI controller
// for ESP32 that is the so-called VSPI, for ESP32-S2/S3 it is now called implicitly
// HSPI, as it has shifted places for these chip generations
// for all generations, this is equivalent to SPI3_HOST in the lower level driver
// For ESP32-C2, the only externally usable HW SPI controller is SPI2, its signal names
// being prefixed with FSPI.
#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
#define SPI_NRF HSPI
#elif CONFIG_IDF_TARGET_ESP32C3
#define SPI_NRF FSPI
#else
#define SPI_NRF VSPI
#endif
#include <SpiManager.h>
InverterSettingsClass InverterSettings;
@ -44,7 +31,10 @@ void InverterSettingsClass::init(Scheduler& scheduler)
if (PinMapping.isValidNrf24Config() || PinMapping.isValidCmt2300Config()) {
if (PinMapping.isValidNrf24Config()) {
SPIClass* spiClass = new SPIClass(SPI_NRF);
auto spi_bus = SpiManagerInst.claim_bus_arduino();
ESP_ERROR_CHECK(spi_bus ? ESP_OK : ESP_FAIL);
SPIClass* spiClass = new SPIClass(*spi_bus);
spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs);
Hoymiles.initNRF(spiClass, pin.nrf24_en, pin.nrf24_irq);
}

View File

@ -7,6 +7,7 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include <Hoymiles.h>
#include <CpuTemperature.h>
MqttHandleDtuClass MqttHandleDtu;
@ -34,8 +35,17 @@ void MqttHandleDtuClass::loop()
MqttSettings.publish("dtu/uptime", String(millis() / 1000));
MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString());
MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname());
MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize()));
MqttSettings.publish("dtu/heap/free", String(ESP.getFreeHeap()));
MqttSettings.publish("dtu/heap/minfree", String(ESP.getMinFreeHeap()));
MqttSettings.publish("dtu/heap/maxalloc", String(ESP.getMaxAllocHeap()));
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
MqttSettings.publish("dtu/rssi", String(WiFi.RSSI()));
MqttSettings.publish("dtu/bssid", WiFi.BSSIDstr());
}
float temperature = CpuTemperature.read();
if (!std::isnan(temperature)) {
MqttSettings.publish("dtu/temperature", String(temperature));
}
}

View File

@ -7,8 +7,8 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "Utils.h"
#include "defaults.h"
#include "__compiled_constants.h"
#include "defaults.h"
MqttHandleHassClass MqttHandleHass;
@ -58,29 +58,45 @@ void MqttHandleHassClass::publishConfig()
const CONFIG_T& config = Configuration.get();
// publish DTU sensors
publishDtuSensor("IP", "", "diagnostic", "mdi:network-outline", "", "");
publishDtuSensor("WiFi Signal", "signal_strength", "diagnostic", "", "dBm", "rssi");
publishDtuSensor("Uptime", "duration", "diagnostic", "", "s", "");
publishDtuBinarySensor("Status", "connectivity", "diagnostic", config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, config.Mqtt.Lwt.Topic);
publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Temperature", "dtu/temperature", "°C", "mdi:thermometer", DEVICE_CLS_TEMPERATURE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
yield();
publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE);
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, CATEGORY_NONE);
publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, CATEGORY_DIAGNOSTIC);
// Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
publishInverterButton(inv, "Turn Inverter Off", "mdi:power-plug-off", "config", "", "cmd/power", "0");
publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1");
publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1");
publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, CATEGORY_CONFIG);
publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1);
publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1);
publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT);
publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT);
publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", CATEGORY_CONFIG);
publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0");
publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0");
publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0", DEVICE_CLS_CONNECTIVITY, CATEGORY_DIAGNOSTIC);
publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, CATEGORY_NONE);
publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) {
@ -94,8 +110,6 @@ void MqttHandleHassClass::publishConfig()
}
}
}
yield();
}
}
@ -128,8 +142,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
if (!clear) {
const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId);
const char* devCls = deviceClasses[fieldType.deviceClsId];
const char* stateCls = stateClasses[fieldType.stateClsId];
const char* stateCls = stateClass_name[fieldType.stateClsId];
String name;
if (type != TYPE_DC) {
@ -138,46 +151,34 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
name = "CH" + chanNum + " " + fieldName;
}
String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId);
JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, CATEGORY_NONE);
root["name"] = name;
root["stat_t"] = stateTopic;
root["uniq_id"] = serial + "_ch" + chanNum + "_" + fieldName;
String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId);
if (unit_of_measure != "") {
root["unit_of_meas"] = unit_of_measure;
}
createInverterInfo(root, inv);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold();
}
if (devCls != 0) {
root["dev_cla"] = devCls;
}
if (stateCls != 0) {
root["stat_cla"] = stateCls;
}
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
publish(configTopic, root);
} else {
publish(configTopic, "");
}
}
void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload)
void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const CategoryType category)
{
const String serial = inv->serialString();
String buttonId = caption;
String buttonId = name;
buttonId.replace(" ", "_");
buttonId.toLowerCase();
@ -185,41 +186,29 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
+ "/" + buttonId
+ "/config";
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + state_topic;
JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, "", icon, device_class, category);
root["name"] = caption;
root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
if (strcmp(deviceClass, "")) {
root["dev_cla"] = deviceClass;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic;
root["payload_press"] = payload;
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
publish(configTopic, root);
}
void MqttHandleHassClass::publishInverterNumber(
std::shared_ptr<InverterAbstract> inv, const char* caption, const char* icon, const char* category,
const char* commandTopic, const char* stateTopic, const char* unitOfMeasure,
const int16_t min, const int16_t max, float step)
std::shared_ptr<InverterAbstract> inv, const String& name,
const String& stateTopic, const String& command_topic,
const int16_t min, const int16_t max, float step,
const String& unit_of_measure, const String& icon, const CategoryType category)
{
const String serial = inv->serialString();
String buttonId = caption;
String buttonId = name;
buttonId.replace(" ", "_");
buttonId.toLowerCase();
@ -227,150 +216,22 @@ void MqttHandleHassClass::publishInverterNumber(
+ "/" + buttonId
+ "/config";
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + commandTopic;
const String cmdTopic = MqttSettings.getPrefix() + serial + "/" + command_topic;
const String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
JsonDocument root;
createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, category);
root["name"] = caption;
root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId;
if (strcmp(icon, "")) {
root["ic"] = icon;
}
root["ent_cat"] = category;
root["cmd_t"] = cmdTopic;
root["stat_t"] = statTopic;
root["unit_of_meas"] = unitOfMeasure;
root["min"] = min;
root["max"] = max;
root["step"] = step;
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off)
{
const String serial = inv->serialString();
String sensorId = caption;
sensorId.replace(" ", "_");
sensorId.toLowerCase();
const String configTopic = "binary_sensor/dtu_" + serial
+ "/" + sensorId
+ "/config";
const String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
JsonDocument root;
root["name"] = caption;
root["uniq_id"] = serial + "_" + sensorId;
root["stat_t"] = statTopic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
createInverterInfo(root, inv);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishDtuSensor(const char* name, const char* device_class, const char* category, const char* icon, const char* unit_of_measure, const char* subTopic)
{
String id = name;
id.toLowerCase();
id.replace(" ", "_");
String topic = subTopic;
if (topic == "") {
topic = id;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
if (strcmp(device_class, "")) {
root["dev_cla"] = device_class;
}
if (strcmp(category, "")) {
root["ent_cat"] = category;
}
if (strcmp(icon, "")) {
root["ic"] = icon;
}
if (strcmp(unit_of_measure, "")) {
root["unit_of_meas"] = unit_of_measure;
}
root["stat_t"] = MqttSettings.getPrefix() + "dtu" + "/" + topic;
root["avty_t"] = MqttSettings.getPrefix() + Configuration.get().Mqtt.Lwt.Topic;
const CONFIG_T& config = Configuration.get();
root["pl_avail"] = config.Mqtt.Lwt.Value_Online;
root["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleHassClass::publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic)
{
String id = name;
id.toLowerCase();
id.replace(" ", "_");
String topic = subTopic;
if (!strcmp(subTopic, "")) {
topic = String("dtu/") + "/" + id;
}
JsonDocument root;
root["name"] = name;
root["uniq_id"] = getDtuUniqueId() + "_" + id;
root["stat_t"] = MqttSettings.getPrefix() + topic;
root["pl_on"] = payload_on;
root["pl_off"] = payload_off;
if (strcmp(device_class, "")) {
root["dev_cla"] = device_class;
}
if (strcmp(category, "")) {
root["ent_cat"] = category;
}
createDtuInfo(root);
if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
const String configTopic = "binary_sensor/" + getDtuUniqueId() + "/" + id + "/config";
serializeJson(root, buffer);
publish(configTopic, buffer);
publish(configTopic, root);
}
void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr<InverterAbstract> inv)
@ -433,4 +294,106 @@ void MqttHandleHassClass::publish(const String& subtopic, const String& payload)
String topic = Configuration.get().Mqtt.Hass.Topic;
topic += subtopic;
MqttSettings.publishGeneric(topic, payload, Configuration.get().Mqtt.Hass.Retain);
yield();
}
void MqttHandleHassClass::publish(const String& subtopic, const JsonDocument& doc)
{
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
String buffer;
serializeJson(doc, buffer);
publish(subtopic, buffer);
}
void MqttHandleHassClass::addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category)
{
if (unit_of_measure != "") {
doc["unit_of_meas"] = unit_of_measure;
}
if (icon != "") {
doc["ic"] = icon;
}
if (device_class != DEVICE_CLS_NONE) {
doc["dev_cla"] = deviceClass_name[device_class];
}
if (category != CATEGORY_NONE) {
doc["ent_cat"] = category_name[category];
}
}
void MqttHandleHassClass::publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category)
{
String sensor_id = name;
sensor_id.toLowerCase();
sensor_id.replace(" ", "_");
doc["name"] = name;
doc["uniq_id"] = unique_id_prefix + "_" + sensor_id;
doc["stat_t"] = MqttSettings.getPrefix() + state_topic;
doc["pl_on"] = payload_on;
doc["pl_off"] = payload_off;
addCommonMetadata(doc, "", "", device_class, category);
const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc);
}
void MqttHandleHassClass::publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category)
{
const String dtuId = getDtuUniqueId();
JsonDocument root;
createDtuInfo(root);
publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, category);
}
void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category)
{
const String serial = inv->serialString();
JsonDocument root;
createInverterInfo(root, inv);
publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, category);
}
void MqttHandleHassClass::publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category)
{
String sensor_id = name;
sensor_id.toLowerCase();
sensor_id.replace(" ", "_");
doc["name"] = name;
doc["uniq_id"] = unique_id_prefix + "_" + sensor_id;
doc["stat_t"] = MqttSettings.getPrefix() + state_topic;
addCommonMetadata(doc, unit_of_measure, icon, device_class, category);
const CONFIG_T& config = Configuration.get();
doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
doc["pl_avail"] = config.Mqtt.Lwt.Value_Online;
doc["pl_not_avail"] = config.Mqtt.Lwt.Value_Offline;
const String configTopic = "sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc);
}
void MqttHandleHassClass::publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category)
{
const String dtuId = getDtuUniqueId();
JsonDocument root;
createDtuInfo(root);
publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, category);
}
void MqttHandleHassClass::publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category)
{
const String serial = inv->serialString();
JsonDocument root;
createInverterInfo(root, inv);
publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, category);
}

View File

@ -7,13 +7,6 @@
#include "MqttSettings.h"
#include <ctime>
#define TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE "limit_persistent_relative"
#define TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE "limit_persistent_absolute"
#define TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE "limit_nonpersistent_relative"
#define TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE "limit_nonpersistent_absolute"
#define TOPIC_SUB_POWER "power"
#define TOPIC_SUB_RESTART "restart"
#define PUBLISH_MAX_INTERVAL 60000
MqttHandleInverterClass MqttHandleInverter;
@ -50,6 +43,14 @@ void MqttHandleInverterClass::loop()
// Name
MqttSettings.publish(subtopic + "/name", inv->name());
// Radio Statistics
MqttSettings.publish(subtopic + "/radio/tx_request", String(inv->RadioStats.TxRequestData));
MqttSettings.publish(subtopic + "/radio/tx_re_request", String(inv->RadioStats.TxReRequestFragment));
MqttSettings.publish(subtopic + "/radio/rx_success", String(inv->RadioStats.RxSuccess));
MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData));
if (inv->DevInfo()->getLastUpdate() > 0) {
// Bootloader Version
MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion()));
@ -146,7 +147,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv,
return inv->serialString() + "/" + chanNum + "/" + chanName;
}
void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
void MqttHandleInverterClass::onMqttMessage(Topic t, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
{
const CONFIG_T& config = Configuration.get();
@ -154,15 +155,11 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
char* serial_str;
char* subtopic;
char* setting;
char* rest = &token_topic[strlen(config.Mqtt.Topic)];
serial_str = strtok_r(rest, "/", &rest);
subtopic = strtok_r(rest, "/", &rest);
setting = strtok_r(rest, "/", &rest);
if (serial_str == NULL || subtopic == NULL || setting == NULL) {
if (serial_str == NULL) {
return;
}
@ -175,33 +172,30 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
return;
}
// check if subtopic is unequal cmd
if (strcmp(subtopic, "cmd")) {
std::string strValue(reinterpret_cast<const char*>(payload), len);
float payload_val = -1;
try {
payload_val = std::stof(strValue);
} catch (std::invalid_argument const& e) {
MessageOutput.printf("MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n",
topic, strValue.c_str());
return;
}
char* strlimit = new char[len + 1];
memcpy(strlimit, payload, len);
strlimit[len] = '\0';
const float payload_val = strtof(strlimit, NULL);
delete[] strlimit;
if (payload_val < 0) {
MessageOutput.printf("MQTT payload < 0 received --> ignoring\r\n");
return;
}
if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) {
switch (t) {
case Topic::LimitPersistentRelative:
// Set inverter limit relative persistent
MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val);
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent);
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) {
case Topic::LimitPersistentAbsolute:
// Set inverter limit absolute persistent
MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val);
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent);
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) {
case Topic::LimitNonPersistentRelative:
// Set inverter limit relative non persistent
MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val);
if (!properties.retain) {
@ -209,8 +203,9 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else {
MessageOutput.println("Ignored because retained");
}
break;
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) {
case Topic::LimitNonPersistentAbsolute:
// Set inverter limit absolute non persistent
MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val);
if (!properties.retain) {
@ -218,13 +213,15 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else {
MessageOutput.println("Ignored because retained");
}
break;
} else if (!strcmp(setting, TOPIC_SUB_POWER)) {
case Topic::Power:
// Turn inverter on or off
MessageOutput.printf("Set inverter power to: %d\r\n", static_cast<int32_t>(payload_val));
inv->sendPowerControlRequest(static_cast<int32_t>(payload_val) > 0);
break;
} else if (!strcmp(setting, TOPIC_SUB_RESTART)) {
case Topic::Restart:
// Restart inverter
MessageOutput.printf("Restart inverter\r\n");
if (!properties.retain && payload_val == 1) {
@ -232,34 +229,41 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
} else {
MessageOutput.println("Ignored because retained or numeric value not '1'");
}
break;
case Topic::ResetRfStats:
// Reset RF Stats
MessageOutput.printf("Reset RF stats\r\n");
if (!properties.retain && payload_val == 1) {
inv->resetRadioStats();
} else {
MessageOutput.println("Ignored because retained or numeric value not '1'");
}
}
}
void MqttHandleInverterClass::subscribeTopics()
{
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;
using std::placeholders::_5;
using std::placeholders::_6;
String const& prefix = MqttSettings.getPrefix();
const String topic = MqttSettings.getPrefix();
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
auto subscribe = [&prefix, this](char const* subTopic, Topic t) {
String fullTopic(prefix + _cmdtopic.data() + subTopic);
MqttSettings.subscribe(fullTopic.c_str(), 0,
std::bind(&MqttHandleInverterClass::onMqttMessage, this, t,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6));
};
for (auto const& s : _subscriptions) {
subscribe(s.first.data(), s.second);
}
}
void MqttHandleInverterClass::unsubscribeTopics()
{
const String topic = MqttSettings.getPrefix();
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER));
MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART));
String const& prefix = MqttSettings.getPrefix() + _cmdtopic.data();
for (auto const& s : _subscriptions) {
MqttSettings.unsubscribe(prefix + s.first.data());
}
}

View File

@ -7,10 +7,10 @@
#include "MessageOutput.h"
#include "PinMapping.h"
#include "Utils.h"
#include "__compiled_constants.h"
#include "defaults.h"
#include <ESPmDNS.h>
#include <ETH.h>
#include "__compiled_constants.h"
NetworkSettingsClass::NetworkSettingsClass()
: _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&NetworkSettingsClass::loop, this))
@ -23,20 +23,34 @@ NetworkSettingsClass::NetworkSettingsClass()
void NetworkSettingsClass::init(Scheduler& scheduler)
{
using std::placeholders::_1;
using std::placeholders::_2;
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
WiFi.disconnect(true, true);
WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1));
WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1, _2));
if (PinMapping.isValidW5500Config()) {
PinMapping_t& pin = PinMapping.get();
_w5500 = std::make_unique<W5500>(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst);
} else if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get();
#if ESP_ARDUINO_VERSION_MAJOR < 3
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
#else
ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode);
#endif
}
setupMode();
scheduler.addTask(_loopTask);
_loopTask.enable();
}
void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event)
void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t info)
{
switch (event) {
case ARDUINO_EVENT_ETH_START:
@ -76,7 +90,8 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event)
}
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
MessageOutput.println("WiFi disconnected");
// Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141
MessageOutput.printf("WiFi disconnected: %d\r\n", info.wifi_sta_disconnected.reason);
if (_networkMode == network_mode::WiFi) {
MessageOutput.println("Try reconnecting");
WiFi.disconnect(true, false);
@ -95,7 +110,7 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event)
}
}
bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event event)
bool NetworkSettingsClass::onEvent(DtuNetworkEventCb cbEvent, const network_event event)
{
if (!cbEvent) {
return pdFALSE;
@ -109,8 +124,7 @@ bool NetworkSettingsClass::onEvent(NetworkEventCb cbEvent, const network_event e
void NetworkSettingsClass::raiseEvent(const network_event event)
{
for (uint32_t i = 0; i < _cbEventList.size(); i++) {
const NetworkEventCbList_t entry = _cbEventList[i];
for (auto& entry : _cbEventList) {
if (entry.cb) {
if (entry.event == event || entry.event == network_event::NETWORK_EVENT_MAX) {
entry.cb(event);
@ -167,11 +181,6 @@ void NetworkSettingsClass::setupMode()
WiFi.mode(WIFI_MODE_NULL);
}
}
if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get();
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
}
}
void NetworkSettingsClass::enableAdminMode()
@ -399,6 +408,9 @@ String NetworkSettingsClass::macAddress() const
{
switch (_networkMode) {
case network_mode::Ethernet:
if (_w5500) {
return _w5500->macAddress();
}
return ETH.macAddress();
break;
case network_mode::WiFi:

View File

@ -84,6 +84,54 @@
#define CMT_SDIO -1
#endif
#ifndef W5500_MOSI
#define W5500_MOSI -1
#endif
#ifndef W5500_MISO
#define W5500_MISO -1
#endif
#ifndef W5500_SCLK
#define W5500_SCLK -1
#endif
#ifndef W5500_CS
#define W5500_CS -1
#endif
#ifndef W5500_INT
#define W5500_INT -1
#endif
#ifndef W5500_RST
#define W5500_RST -1
#endif
#ifndef ETH_PHY_ADDR
#define ETH_PHY_ADDR -1
#endif
#ifndef ETH_PHY_POWER
#define ETH_PHY_POWER -1
#endif
#ifndef ETH_PHY_MDC
#define ETH_PHY_MDC -1
#endif
#ifndef ETH_PHY_MDIO
#define ETH_PHY_MDIO -1
#endif
#ifndef ETH_PHY_TYPE
#define ETH_PHY_TYPE ETH_PHY_LAN8720
#endif
#ifndef ETH_CLK_MODE
#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN
#endif
PinMappingClass PinMapping;
PinMappingClass::PinMappingClass()
@ -103,6 +151,13 @@ PinMappingClass::PinMappingClass()
_pinMapping.cmt_gpio3 = CMT_GPIO3;
_pinMapping.cmt_sdio = CMT_SDIO;
_pinMapping.w5500_mosi = W5500_MOSI;
_pinMapping.w5500_miso = W5500_MISO;
_pinMapping.w5500_sclk = W5500_SCLK;
_pinMapping.w5500_cs = W5500_CS;
_pinMapping.w5500_int = W5500_INT;
_pinMapping.w5500_rst = W5500_RST;
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = true;
#else
@ -164,6 +219,13 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.cmt_gpio3 = doc[i]["cmt"]["gpio3"] | CMT_GPIO3;
_pinMapping.cmt_sdio = doc[i]["cmt"]["sdio"] | CMT_SDIO;
_pinMapping.w5500_mosi = doc[i]["w5500"]["mosi"] | W5500_MOSI;
_pinMapping.w5500_miso = doc[i]["w5500"]["miso"] | W5500_MISO;
_pinMapping.w5500_sclk = doc[i]["w5500"]["sclk"] | W5500_SCLK;
_pinMapping.w5500_cs = doc[i]["w5500"]["cs"] | W5500_CS;
_pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT;
_pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST;
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true;
#else
@ -211,7 +273,19 @@ bool PinMappingClass::isValidCmt2300Config() const
&& _pinMapping.cmt_sdio >= 0;
}
bool PinMappingClass::isValidW5500Config() const
{
return _pinMapping.w5500_mosi >= 0
&& _pinMapping.w5500_miso >= 0
&& _pinMapping.w5500_sclk >= 0
&& _pinMapping.w5500_cs >= 0
&& _pinMapping.w5500_int >= 0
&& _pinMapping.w5500_rst >= 0;
}
bool PinMappingClass::isValidEthConfig() const
{
return _pinMapping.eth_enabled;
return _pinMapping.eth_enabled
&& _pinMapping.eth_mdc >= 0
&& _pinMapping.eth_mdio >= 0;
}

36
src/RestartHelper.cpp Normal file
View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "RestartHelper.h"
#include "Display_Graphic.h"
#include "Led_Single.h"
#include <Esp.h>
RestartHelperClass RestartHelper;
RestartHelperClass::RestartHelperClass()
: _rebootTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&RestartHelperClass::loop, this))
{
}
void RestartHelperClass::init(Scheduler& scheduler)
{
scheduler.addTask(_rebootTask);
}
void RestartHelperClass::triggerRestart()
{
_rebootTask.enable();
_rebootTask.restart();
}
void RestartHelperClass::loop()
{
if (_rebootTask.isFirstIteration()) {
LedSingle.turnAllOff();
Display.setStatus(false);
} else {
ESP.restart();
}
}

View File

@ -4,10 +4,8 @@
*/
#include "Utils.h"
#include "Display_Graphic.h"
#include "Led_Single.h"
#include "MessageOutput.h"
#include <Esp.h>
#include "PinMapping.h"
#include <LittleFS.h>
uint32_t Utils::getChipId()
@ -59,16 +57,6 @@ int Utils::getTimezoneOffset()
return static_cast<int>(difftime(rawtime, gmt));
}
void Utils::restartDtu()
{
LedSingle.turnAllOff();
Display.setStatus(false);
yield();
delay(1000);
yield();
ESP.restart();
}
bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line)
{
if (doc.overflowed()) {

111
src/W5500.cpp Normal file
View File

@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2024 Thomas Basler and others
*/
#include "W5500.h"
#include <SpiManager.h>
#include <driver/spi_master.h>
// Internal Arduino functions from WiFiGeneric
void tcpipInit();
void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
W5500::W5500(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst)
: eth_handle(nullptr)
, eth_netif(nullptr)
{
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_reset_pin(static_cast<gpio_num_t>(pin_cs));
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
esp_err_t err = gpio_install_isr_service(ARDUINO_ISR_FLAG);
if (err != ESP_ERR_INVALID_STATE) // don't raise an error when ISR service is already installed
ESP_ERROR_CHECK(err);
auto bus_config = std::make_shared<SpiBusConfig>(
static_cast<gpio_num_t>(pin_mosi),
static_cast<gpio_num_t>(pin_miso),
static_cast<gpio_num_t>(pin_sclk));
spi_device_interface_config_t device_config {
.command_bits = 16, // actually address phase
.address_bits = 8, // actually command phase
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0, // only 0 supported
.cs_ena_posttrans = 0, // only 0 supported
.clock_speed_hz = 20000000, // stable with OpenDTU Fusion shield
.input_delay_ns = 0,
.spics_io_num = pin_cs,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
spi_device_handle_t spi = SpiManagerInst.alloc_device("", bus_config, device_config);
if (!spi)
ESP_ERROR_CHECK(ESP_FAIL);
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
// Arduino function to start networking stack if not already started
tcpipInit();
ESP_ERROR_CHECK(tcpip_adapter_set_default_eth_handlers());
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(spi);
w5500_config.int_gpio_num = pin_int;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
mac_config.rx_task_stack_size = 4096;
esp_eth_mac_t* mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.reset_gpio_num = -1;
esp_eth_phy_t* phy = esp_eth_phy_new_w5500(&phy_config);
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
ESP_ERROR_CHECK(esp_eth_driver_install(&eth_config, &eth_handle));
// Configure MAC address
uint8_t mac_addr[6];
ESP_ERROR_CHECK(esp_read_mac(mac_addr, ESP_MAC_ETH));
ESP_ERROR_CHECK(esp_eth_ioctl(eth_handle, ETH_CMD_S_MAC_ADDR, mac_addr));
esp_netif_config_t netif_config = ESP_NETIF_DEFAULT_ETH();
eth_netif = esp_netif_new(&netif_config);
ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(eth_handle)));
// Add to Arduino
add_esp_interface_netif(ESP_IF_ETH, eth_netif);
ESP_ERROR_CHECK(esp_eth_start(eth_handle));
}
W5500::~W5500()
{
// TODO(LennartF22): support cleanup at some point?
}
String W5500::macAddress()
{
uint8_t mac_addr[6] = {};
esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
char mac_addr_str[18];
snprintf(
mac_addr_str, sizeof(mac_addr_str), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return String(mac_addr_str);
}

View File

@ -4,6 +4,7 @@
*/
#include "WebApi_config.h"
#include "Configuration.h"
#include "RestartHelper.h"
#include "Utils.h"
#include "WebApi.h"
#include "WebApi_errors.h"
@ -61,7 +62,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("delete"))) {
if (!(root["delete"].is<bool>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -82,7 +83,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Utils::removeAllFiles();
Utils::restartDtu();
RestartHelper.triggerRestart();
}
void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
@ -124,7 +125,7 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
Utils::restartDtu();
RestartHelper.triggerRestart();
}
void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)

View File

@ -6,7 +6,7 @@
#include "Configuration.h"
#include "Display_Graphic.h"
#include "PinMapping.h"
#include "Utils.h"
#include "RestartHelper.h"
#include "WebApi.h"
#include "WebApi_errors.h"
#include "helper.h"
@ -50,6 +50,14 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
cmtPinObj["gpio2"] = pin.cmt_gpio2;
cmtPinObj["gpio3"] = pin.cmt_gpio3;
auto w5500PinObj = curPin["w5500"].to<JsonObject>();
w5500PinObj["sclk"] = pin.w5500_sclk;
w5500PinObj["mosi"] = pin.w5500_mosi;
w5500PinObj["miso"] = pin.w5500_miso;
w5500PinObj["cs"] = pin.w5500_cs;
w5500PinObj["int"] = pin.w5500_int;
w5500PinObj["rst"] = pin.w5500_rst;
auto ethPinObj = curPin["eth"].to<JsonObject>();
ethPinObj["enabled"] = pin.eth_enabled;
ethPinObj["phy_addr"] = pin.eth_phy_addr;
@ -103,8 +111,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("curPin")
|| root.containsKey("display"))) {
if (!(root["curPin"].is<JsonObject>()
|| root["display"].is<JsonObject>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -149,6 +157,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
if (performRestart) {
Utils::restartDtu();
RestartHelper.triggerRestart();
}
}

View File

@ -90,12 +90,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("serial")
&& root.containsKey("pollinterval")
&& root.containsKey("nrf_palevel")
&& root.containsKey("cmt_palevel")
&& root.containsKey("cmt_frequency")
&& root.containsKey("cmt_country"))) {
if (!(root["serial"].is<String>()
&& root["pollinterval"].is<uint32_t>()
&& root["nrf_palevel"].is<uint8_t>()
&& root["cmt_palevel"].is<int8_t>()
&& root["cmt_frequency"].is<uint32_t>()
&& root["cmt_country"].is<uint8_t>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -4,6 +4,7 @@
*/
#include "WebApi_firmware.h"
#include "Configuration.h"
#include "RestartHelper.h"
#include "Update.h"
#include "Utils.h"
#include "WebApi.h"
@ -37,7 +38,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request)
response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
Utils::restartDtu();
RestartHelper.triggerRestart();
}
void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)

View File

@ -21,6 +21,7 @@ void WebApiInverterClass::init(AsyncWebServer& server, Scheduler& scheduler)
server.on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1));
server.on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1));
server.on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1));
server.on("/api/inverter/stats_reset", HTTP_GET, std::bind(&WebApiInverterClass::onInverterStatReset, this, _1));
}
void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
@ -95,8 +96,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("serial")
&& root.containsKey("name"))) {
if (!(root["serial"].is<String>()
&& root["name"].is<String>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -165,7 +166,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
if (!(root["id"].is<uint8_t>()
&& root["serial"].is<String>()
&& root["name"].is<String>()
&& root["channel"].is<JsonArray>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -281,7 +285,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("id"))) {
if (!(root["id"].is<uint8_t>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -323,7 +327,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("order"))) {
if (!(root["order"].is<JsonArray>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -346,3 +350,24 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiInverterClass::onInverterStatReset(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
auto retMsg = response->getRoot();
auto serial = WebApi.parseSerialFromRequest(request);
auto inv = Hoymiles.getInverterBySerial(serial);
if (inv != nullptr) {
inv->resetRadioStats();
retMsg["type"] = "success";
retMsg["message"] = "Stats resetted";
retMsg["code"] = WebApiError::InverterStatsResetted;
}
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

View File

@ -64,9 +64,9 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("serial")
&& root.containsKey("limit_value")
&& root.containsKey("limit_type"))) {
if (!(root["serial"].is<String>()
&& root["limit_value"].is<float>()
&& root["limit_type"].is<uint16_t>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -4,7 +4,7 @@
*/
#include "WebApi_maintenance.h"
#include "Utils.h"
#include "RestartHelper.h"
#include "WebApi.h"
#include "WebApi_errors.h"
#include <AsyncJson.h>
@ -30,7 +30,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("reboot"))) {
if (!(root["reboot"].is<bool>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -43,7 +43,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
retMsg["code"] = WebApiError::MaintenanceRebootTriggered;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Utils::restartDtu();
RestartHelper.triggerRestart();
} else {
retMsg["message"] = "Reboot cancled!";
retMsg["code"] = WebApiError::MaintenanceRebootCancled;

View File

@ -107,29 +107,29 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("mqtt_enabled")
&& root.containsKey("mqtt_hostname")
&& root.containsKey("mqtt_port")
&& root.containsKey("mqtt_clientid")
&& root.containsKey("mqtt_username")
&& root.containsKey("mqtt_password")
&& root.containsKey("mqtt_topic")
&& root.containsKey("mqtt_retain")
&& root.containsKey("mqtt_tls")
&& root.containsKey("mqtt_tls_cert_login")
&& root.containsKey("mqtt_client_cert")
&& root.containsKey("mqtt_client_key")
&& root.containsKey("mqtt_lwt_topic")
&& root.containsKey("mqtt_lwt_online")
&& root.containsKey("mqtt_lwt_offline")
&& root.containsKey("mqtt_lwt_qos")
&& root.containsKey("mqtt_publish_interval")
&& root.containsKey("mqtt_clean_session")
&& root.containsKey("mqtt_hass_enabled")
&& root.containsKey("mqtt_hass_expire")
&& root.containsKey("mqtt_hass_retain")
&& root.containsKey("mqtt_hass_topic")
&& root.containsKey("mqtt_hass_individualpanels"))) {
if (!(root["mqtt_enabled"].is<bool>()
&& root["mqtt_hostname"].is<String>()
&& root["mqtt_port"].is<uint>()
&& root["mqtt_clientid"].is<String>()
&& root["mqtt_username"].is<String>()
&& root["mqtt_password"].is<String>()
&& root["mqtt_topic"].is<String>()
&& root["mqtt_retain"].is<bool>()
&& root["mqtt_tls"].is<bool>()
&& root["mqtt_tls_cert_login"].is<bool>()
&& root["mqtt_client_cert"].is<String>()
&& root["mqtt_client_key"].is<String>()
&& root["mqtt_lwt_topic"].is<String>()
&& root["mqtt_lwt_online"].is<String>()
&& root["mqtt_lwt_offline"].is<String>()
&& root["mqtt_lwt_qos"].is<uint8_t>()
&& root["mqtt_publish_interval"].is<uint32_t>()
&& root["mqtt_clean_session"].is<bool>()
&& root["mqtt_hass_enabled"].is<bool>()
&& root["mqtt_hass_expire"].is<bool>()
&& root["mqtt_hass_retain"].is<bool>()
&& root["mqtt_hass_topic"].is<String>()
&& root["mqtt_hass_individualpanels"].is<bool>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -88,16 +88,16 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("ssid")
&& root.containsKey("password")
&& root.containsKey("hostname")
&& root.containsKey("dhcp")
&& root.containsKey("ipaddress")
&& root.containsKey("netmask")
&& root.containsKey("gateway")
&& root.containsKey("dns1")
&& root.containsKey("dns2")
&& root.containsKey("aptimeout"))) {
if (!(root["ssid"].is<String>()
&& root["password"].is<String>()
&& root["hostname"].is<String>()
&& root["dhcp"].is<bool>()
&& root["ipaddress"].is<String>()
&& root["netmask"].is<String>()
&& root["gateway"].is<String>()
&& root["dns1"].is<String>()
&& root["dns2"].is<String>()
&& root["aptimeout"].is<uint>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -100,11 +100,11 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("ntp_server")
&& root.containsKey("ntp_timezone")
&& root.containsKey("longitude")
&& root.containsKey("latitude")
&& root.containsKey("sunsettype"))) {
if (!(root["ntp_server"].is<String>()
&& root["ntp_timezone"].is<String>()
&& root["longitude"].is<double>()
&& root["latitude"].is<double>()
&& root["sunsettype"].is<uint8_t>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -193,12 +193,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("year")
&& root.containsKey("month")
&& root.containsKey("day")
&& root.containsKey("hour")
&& root.containsKey("minute")
&& root.containsKey("second"))) {
if (!(root["year"].is<uint>()
&& root["month"].is<uint>()
&& root["day"].is<uint>()
&& root["hour"].is<uint>()
&& root["minute"].is<uint>()
&& root["second"].is<uint>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -57,9 +57,9 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!(root.containsKey("serial")
&& (root.containsKey("power")
|| root.containsKey("restart")))) {
if (!(root["serial"].is<String>()
&& (root["power"].is<bool>()
|| root["restart"].is<bool>()))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -84,8 +84,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
return;
}
if (root.containsKey("power")) {
uint16_t power = root["power"].as<bool>();
if (root["power"].is<bool>()) {
bool power = root["power"].as<bool>();
inv->sendPowerControlRequest(power);
} else {
if (root["restart"].as<bool>()) {

View File

@ -48,8 +48,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot();
if (!root.containsKey("password")
&& root.containsKey("allow_readonly")) {
if (!root["password"].is<String>()
&& root["allow_readonly"].is<bool>()) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

View File

@ -134,6 +134,12 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std
} else {
root["limit_absolute"] = -1;
}
root["radio_stats"]["tx_request"] = inv->RadioStats.TxRequestData;
root["radio_stats"]["tx_re_request"] = inv->RadioStats.TxReRequestFragment;
root["radio_stats"]["rx_success"] = inv->RadioStats.RxSuccess;
root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer;
root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer;
root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData;
}
void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)

View File

@ -16,6 +16,7 @@
#include "NetworkSettings.h"
#include "NtpSettings.h"
#include "PinMapping.h"
#include "RestartHelper.h"
#include "Scheduler.h"
#include "SunPosition.h"
#include "Utils.h"
@ -25,12 +26,21 @@
#include <LittleFS.h>
#include <TaskScheduler.h>
#include <esp_heap_caps.h>
#include <SpiManager.h>
#include <driver/uart.h>
void setup()
{
// Move all dynamic allocations >512byte to psram (if available)
heap_caps_malloc_extmem_enable(512);
// Initialize SpiManager
SpiManagerInst.register_bus(SPI2_HOST);
#if SOC_SPI_PERIPH_NUM > 2
SpiManagerInst.register_bus(SPI3_HOST);
#endif
// Initialize serial output
Serial.begin(SERIAL_BAUDRATE);
#if ARDUINO_USB_CDC_ON_BOOT
@ -154,6 +164,7 @@ void setup()
InverterSettings.init(scheduler);
Datastore.init(scheduler);
RestartHelper.init(scheduler);
}
void loop()

View File

@ -17,34 +17,35 @@
"bootstrap": "^5.3.3",
"bootstrap-icons-vue": "^1.11.3",
"mitt": "^3.0.1",
"sortablejs": "^1.15.2",
"sortablejs": "^1.15.3",
"spark-md5": "^3.0.2",
"vue": "^3.4.35",
"vue-i18n": "^9.13.1",
"vue-router": "^4.4.2"
"vue": "^3.5.10",
"vue-i18n": "9.13.1",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0",
"@tsconfig/node18": "^18.2.4",
"@tsconfig/node22": "^22.0.0",
"@types/bootstrap": "^5.2.10",
"@types/node": "^22.1.0",
"@types/node": "^22.7.4",
"@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.8",
"@types/spark-md5": "^3.0.4",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.8.0",
"eslint-plugin-vue": "^9.27.0",
"eslint": "^9.11.1",
"eslint-plugin-vue": "^9.28.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.77.6",
"terser": "^5.31.3",
"typescript": "^5.5.4",
"vite": "^5.3.5",
"terser": "^5.34.0",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.5.1",
"vue-tsc": "^2.0.29"
}
"vite-plugin-css-injected-by-js": "^3.5.2",
"vue-tsc": "^2.1.6"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@ -1,10 +1,12 @@
<template>
<table class="table table-hover">
<thead>
<th scope="col">{{ $t('eventlog.Start') }}</th>
<th scope="col">{{ $t('eventlog.Stop') }}</th>
<th scope="col">{{ $t('eventlog.Id') }}</th>
<th scope="col">{{ $t('eventlog.Message') }}</th>
<tr>
<th scope="col">{{ $t('eventlog.Start') }}</th>
<th scope="col">{{ $t('eventlog.Stop') }}</th>
<th scope="col">{{ $t('eventlog.Id') }}</th>
<th scope="col">{{ $t('eventlog.Message') }}</th>
</tr>
</thead>
<tbody>
<template v-for="event in eventLogList.count" :key="event">

View File

@ -141,7 +141,16 @@
"Unknown": "Unbekannt",
"ShowGridProfile": "Zeige Grid Profil",
"GridProfile": "Grid Profil",
"LoadingInverter": "Warte auf Daten... (kann bis zu 10 Sekunden dauern)"
"LoadingInverter": "Warte auf Daten... (kann bis zu 10 Sekunden dauern)",
"RadioStats": "Funkstatistik",
"TxRequest": "Gesendete Anfragen",
"RxSuccess": "Empfang Erfolgreich",
"RxFailNothing": "Empfang Fehler: Nichts empfangen",
"RxFailPartial": "Empfang Fehler: Teilweise empfangen",
"RxFailCorrupt": "Empfang Fehler: Beschädigt empfangen",
"TxReRequest": "Gesendete Fragment Wiederanforderungen",
"StatsReset": "Statistiken zurücksetzen",
"StatsResetting": "Zurücksetzen..."
},
"eventlog": {
"Start": "Beginn",

View File

@ -141,7 +141,16 @@
"Unknown": "Unknown",
"ShowGridProfile": "Show Grid Profile",
"GridProfile": "Grid Profile",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)"
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting..."
},
"eventlog": {
"Start": "Start",

View File

@ -141,7 +141,16 @@
"Unknown": "Inconnu",
"ShowGridProfile": "Show Grid Profile",
"GridProfile": "Grid Profile",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)"
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting..."
},
"eventlog": {
"Start": "Départ",

View File

@ -21,6 +21,15 @@ export interface InverterStatistics {
Irradiation?: ValueObject;
}
export interface RadioStatistics {
tx_request: number;
tx_re_request: number;
rx_success: number;
rx_fail_nothing: number;
rx_fail_partial: number;
rx_fail_corrupt: number;
}
export interface Inverter {
serial: string;
name: string;
@ -35,6 +44,7 @@ export interface Inverter {
AC: InverterStatistics[];
DC: InverterStatistics[];
INV: InverterStatistics[];
radio_stats: RadioStatistics;
}
export interface Total {

View File

@ -202,7 +202,7 @@
min="0"
max="100"
id="inputDisplayContrast"
v-model="deviceConfigList.display.contrast"
v-model.number="deviceConfigList.display.contrast"
/>
</div>
</div>
@ -239,7 +239,7 @@
min="0"
max="100"
:id="getLedIdFromNumber(index)"
v-model="ledSetting.brightness"
v-model.number="ledSetting.brightness"
@change="syncSliders"
/>
</div>

View File

@ -48,7 +48,7 @@
<input
type="range"
class="form-control form-range"
v-model="dtuConfigList.cmt_palevel"
v-model.number="dtuConfigList.cmt_palevel"
min="-10"
max="20"
id="inputCmtPaLevel"
@ -89,7 +89,7 @@
<input
type="range"
class="form-control form-range"
v-model="dtuConfigList.cmt_frequency"
v-model.number="dtuConfigList.cmt_frequency"
:min="cmtMinFrequency"
:max="cmtMaxFrequency"
:step="dtuConfigList.cmt_chan_width"

View File

@ -201,6 +201,7 @@
</template>
</template>
</div>
<BootstrapAlert class="m-3" :show="!inverter.hasOwnProperty('INV')">
<div class="d-flex justify-content-center align-items-center">
<div class="spinner-border m-1" role="status">
@ -209,6 +210,110 @@
<span>{{ $t('home.LoadingInverter') }}</span>
</div>
</BootstrapAlert>
<div class="accordion mt-5" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapseStats"
aria-expanded="true"
aria-controls="collapseStats"
>
<BIconBroadcast />&nbsp;{{ $t('home.RadioStats') }}
</button>
</h2>
<div
id="collapseStats"
class="accordion-collapse collapse"
data-bs-parent="#accordionExample"
>
<div class="accordion-body">
<table class="table table-striped table-hover">
<tbody>
<tr>
<td>{{ $t('home.TxRequest') }}</td>
<td>{{ $n(inverter.radio_stats.tx_request) }}</td>
<td></td>
</tr>
<tr>
<td>{{ $t('home.RxSuccess') }}</td>
<td>{{ $n(inverter.radio_stats.rx_success) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_success,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailNothing') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_nothing) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_nothing,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailPartial') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_partial) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_partial,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.RxFailCorrupt') }}</td>
<td>{{ $n(inverter.radio_stats.rx_fail_corrupt) }}</td>
<td>
{{
ratio(
inverter.radio_stats.rx_fail_corrupt,
inverter.radio_stats.tx_request
)
}}
</td>
</tr>
<tr>
<td>{{ $t('home.TxReRequest') }}</td>
<td>{{ $n(inverter.radio_stats.tx_re_request) }}</td>
<td></td>
</tr>
</tbody>
</table>
<button
:disabled="!isLogged || performRadioStatsReset"
type="button"
class="btn btn-danger"
@click="onResetRadioStats(inverter.serial)"
>
<template v-if="!performRadioStatsReset">
{{ $t('home.StatsReset') }}
</template>
<template v-else>
<span
class="spinner-border spinner-border-sm"
aria-hidden="true"
></span>
<span role="status"> {{ $t('home.StatsResetting') }}</span>
</template>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -390,6 +495,7 @@ import { authHeader, authUrl, handleResponse, isLoggedIn } from '@/utils/authent
import * as bootstrap from 'bootstrap';
import {
BIconArrowCounterclockwise,
BIconBroadcast,
BIconCheckCircleFill,
BIconCpu,
BIconExclamationCircleFill,
@ -415,6 +521,7 @@ export default defineComponent({
InverterTotalInfo,
ModalDialog,
BIconArrowCounterclockwise,
BIconBroadcast,
BIconCheckCircleFill,
BIconCpu,
BIconExclamationCircleFill,
@ -461,6 +568,7 @@ export default defineComponent({
alertMessageLimit: '',
alertTypeLimit: 'info',
showAlertLimit: false,
performRadioStatsReset: false,
powerSettingView: {} as bootstrap.Modal,
powerSettingSerial: '',
@ -690,6 +798,14 @@ export default defineComponent({
this.limitSettingView.show();
},
onResetRadioStats(serial: string) {
this.performRadioStatsReset = true;
fetch('/api/inverter/stats_reset?inv=' + serial, { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(() => {
this.performRadioStatsReset = false;
});
},
onSetLimitSettings(setPersistent: boolean) {
this.targetLimitList.limit_type = (setPersistent ? 256 : 0) + this.targetLimitType;
const formData = new FormData();
@ -786,6 +902,12 @@ export default defineComponent({
});
return total;
},
ratio(val_small: number, val_large: number): string {
if (val_large == 0) {
return '-';
}
return this.$n(val_small / val_large, 'percent');
},
},
});
</script>

View File

@ -52,9 +52,7 @@ export default defineComponent({
.then((data) => {
this.systemDataList = data;
this.dataLoading = false;
if (this.allowVersionInfo) {
this.getUpdateInfo();
}
this.getUpdateInfo();
});
},
getUpdateInfo() {
@ -76,6 +74,10 @@ export default defineComponent({
this.systemDataList.git_is_hash = true;
}
if (!this.allowVersionInfo) {
return;
}
const fetchUrl =
'https://api.github.com/repos/tbnobody/OpenDTU/compare/' + this.systemDataList.git_hash + '...HEAD';

View File

@ -1,6 +1,6 @@
{
"extends": [
"@tsconfig/node18/tsconfig.json",
"@tsconfig/node22/tsconfig.json",
"@vue/tsconfig/tsconfig.json"
],
"include": [
@ -15,4 +15,4 @@
"node"
]
}
}
}

View File

@ -6,7 +6,7 @@
"paths": {
"@/*": ["./src/*"]
},
"lib": ["ES2021", "DOM"],
"lib": ["ES2023", "DOM"],
"moduleResolution": "Node",
/* Linting */

View File

@ -45,6 +45,7 @@ export default defineConfig({
outDir: '../webapp_dist',
emptyOutDir: true,
minify: 'terser',
chunkSizeWarningLimit: 1024,
rollupOptions: {
output: {
// Only create one js file

View File

@ -7,6 +7,16 @@
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
"@babel/helper-string-parser@^7.24.8":
version "7.24.8"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d"
integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
"@babel/helper-validator-identifier@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
"@babel/parser@^7.16.4":
version "7.18.11"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9"
@ -17,10 +27,21 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b"
integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==
"@babel/parser@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
"@babel/parser@^7.25.3":
version "7.25.6"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f"
integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==
dependencies:
"@babel/types" "^7.25.6"
"@babel/types@^7.25.6":
version "7.25.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6"
integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==
dependencies:
"@babel/helper-string-parser" "^7.24.8"
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@esbuild/aix-ppc64@0.21.5":
version "0.21.5"
@ -161,15 +182,20 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c"
integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==
"@eslint/config-array@^0.17.1":
version "0.17.1"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.1.tgz#d9b8b8b6b946f47388f32bedfd3adf29ca8f8910"
integrity sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==
"@eslint/config-array@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d"
integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==
dependencies:
"@eslint/object-schema" "^2.1.4"
debug "^4.3.1"
minimatch "^3.1.2"
"@eslint/core@^0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.6.0.tgz#9930b5ba24c406d67a1760e94cdbac616a6eb674"
integrity sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==
"@eslint/eslintrc@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
@ -185,16 +211,23 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.8.0.tgz#ae9bc14bb839713c5056f5018bcefa955556d3a4"
integrity sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==
"@eslint/js@9.11.1":
version "9.11.1"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.11.1.tgz#8bcb37436f9854b3d9a561440daf916acd940986"
integrity sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==
"@eslint/object-schema@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843"
integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==
"@eslint/plugin-kit@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz#8712dccae365d24e9eeecb7b346f85e750ba343d"
integrity sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==
dependencies:
levn "^0.4.1"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
@ -304,10 +337,10 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.17"
@ -357,75 +390,90 @@
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@rollup/rollup-android-arm-eabi@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b"
integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==
"@rollup/rollup-android-arm-eabi@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz#0412834dc423d1ff7be4cb1fc13a86a0cd262c11"
integrity sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==
"@rollup/rollup-android-arm64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e"
integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==
"@rollup/rollup-android-arm64@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz#baf1a014b13654f3b9e835388df9caf8c35389cb"
integrity sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==
"@rollup/rollup-darwin-arm64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b"
integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==
"@rollup/rollup-darwin-arm64@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz#0a2c364e775acdf1172fe3327662eec7c46e55b1"
integrity sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==
"@rollup/rollup-darwin-x64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455"
integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==
"@rollup/rollup-darwin-x64@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz#a972db75890dfab8df0da228c28993220a468c42"
integrity sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==
"@rollup/rollup-linux-arm-gnueabihf@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4"
integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==
"@rollup/rollup-linux-arm-gnueabihf@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz#1609d0630ef61109dd19a278353e5176d92e30a1"
integrity sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==
"@rollup/rollup-linux-arm64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143"
integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==
"@rollup/rollup-linux-arm-musleabihf@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz#3c1dca5f160aa2e79e4b20ff6395eab21804f266"
integrity sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==
"@rollup/rollup-linux-arm64-musl@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef"
integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==
"@rollup/rollup-linux-arm64-gnu@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz#c2fe376e8b04eafb52a286668a8df7c761470ac7"
integrity sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==
"@rollup/rollup-linux-riscv64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6"
integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==
"@rollup/rollup-linux-arm64-musl@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz#e62a4235f01e0f66dbba587c087ca6db8008ec80"
integrity sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==
"@rollup/rollup-linux-x64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778"
integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==
"@rollup/rollup-linux-powerpc64le-gnu@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz#24b3457e75ee9ae5b1c198bd39eea53222a74e54"
integrity sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==
"@rollup/rollup-linux-x64-musl@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89"
integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==
"@rollup/rollup-linux-riscv64-gnu@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz#38edfba9620fe2ca8116c97e02bd9f2d606bde09"
integrity sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==
"@rollup/rollup-win32-arm64-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b"
integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==
"@rollup/rollup-linux-s390x-gnu@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz#a3bfb8bc5f1e802f8c76cff4a4be2e9f9ac36a18"
integrity sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==
"@rollup/rollup-win32-ia32-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2"
integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==
"@rollup/rollup-linux-x64-gnu@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz#0dadf34be9199fcdda44b5985a086326344f30ad"
integrity sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==
"@rollup/rollup-win32-x64-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10"
integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==
"@rollup/rollup-linux-x64-musl@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz#7b7deddce240400eb87f2406a445061b4fed99a8"
integrity sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==
"@tsconfig/node18@^18.2.4":
version "18.2.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.4.tgz#094efbdd70f697d37c09f34067bf41bc4a828ae3"
integrity sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ==
"@rollup/rollup-win32-arm64-msvc@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz#a0ca0c5149c2cfb26fab32e6ba3f16996fbdb504"
integrity sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==
"@rollup/rollup-win32-ia32-msvc@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz#aae2886beec3024203dbb5569db3a137bc385f8e"
integrity sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==
"@rollup/rollup-win32-x64-msvc@4.21.2":
version "4.21.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz#e4291e3c1bc637083f87936c333cdbcad22af63b"
integrity sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==
"@tsconfig/node22@^22.0.0":
version "22.0.0"
resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.0.tgz#0bdaf702f2b7594383d24d7b2b8d557dcfdca1ed"
integrity sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg==
"@types/bootstrap@^5.2.10":
version "5.2.10"
@ -444,17 +492,27 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
"@types/estree@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/json-schema@^7.0.12":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/node@^22.1.0":
version "22.1.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b"
integrity sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/node@^22.7.4":
version "22.7.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc"
integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==
dependencies:
undici-types "~6.13.0"
undici-types "~6.19.2"
"@types/pulltorefreshjs@^0.1.7":
version "0.1.7"
@ -562,29 +620,29 @@
"@typescript-eslint/types" "7.2.0"
eslint-visitor-keys "^3.4.1"
"@vitejs/plugin-vue@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz#f11091e0130eca6c1ca8cfb85ee71ea53b255d31"
integrity sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==
"@vitejs/plugin-vue@^5.1.4":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz#72b8b705cfce36b00b59af196195146e356500c4"
integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
"@volar/language-core@2.4.0-alpha.18", "@volar/language-core@~2.4.0-alpha.18":
version "2.4.0-alpha.18"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz#dafffd68ac07c26d69de16741187fd4c06bfa345"
integrity sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==
"@volar/language-core@2.4.1", "@volar/language-core@~2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.1.tgz#267984b2b06908b78f1c016392fc75b75516595b"
integrity sha512-9AKhC7Qn2mQYxj7Dz3bVxeOk7gGJladhWixUYKef/o0o7Bm4an+A3XvmcTHVqZ8stE6lBVH++g050tBtJ4TZPQ==
dependencies:
"@volar/source-map" "2.4.0-alpha.18"
"@volar/source-map" "2.4.1"
"@volar/source-map@2.4.0-alpha.18":
version "2.4.0-alpha.18"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz#a2413932ff6b1821ae8efcbd9249d4da3f99f223"
integrity sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==
"@volar/source-map@2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.1.tgz#6a6d02b9dac66a5dd99378dcdae63107a0b45fce"
integrity sha512-Xq6ep3OZg9xUqN90jEgB9ztX5SsTz1yiV8wiQbcYNjWkek+Ie3dc8l7AVt3EhDm9mSIR58oWczHkzM2H6HIsmQ==
"@volar/typescript@~2.4.0-alpha.18":
version "2.4.0-alpha.18"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz#806aca9ce1bd7c48dc5fcd0fcf7f33bdd04e5b35"
integrity sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==
"@volar/typescript@~2.4.1":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.1.tgz#6285f29b36c58769ccc14153f329d11e89ee13bc"
integrity sha512-UoRzC0PXcwajFQTu8XxKSYNsWNBtVja6Y9gC8eLv7kYm+UEKJCcZ8g7dialsOYA0HKs3Vpg57MeCsawFLC6m9Q==
dependencies:
"@volar/language-core" "2.4.0-alpha.18"
"@volar/language-core" "2.4.1"
path-browserify "^1.0.1"
vscode-uri "^3.0.8"
@ -609,13 +667,13 @@
estree-walker "^2.0.2"
source-map-js "^1.0.2"
"@vue/compiler-core@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.35.tgz#421922a75ecabf1aabc6b7a2ce98b5acb2fc2d65"
integrity sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg==
"@vue/compiler-core@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.10.tgz#dc382e4173c5ad6d309887f5cb02983dfd88cfee"
integrity sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==
dependencies:
"@babel/parser" "^7.24.7"
"@vue/shared" "3.4.35"
"@babel/parser" "^7.25.3"
"@vue/shared" "3.5.10"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
@ -628,13 +686,13 @@
"@vue/compiler-core" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-dom@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.35.tgz#cd0881f1b4ed655cd96367bce4845f87023a5a2d"
integrity sha512-pWIZRL76/oE/VMhdv/ovZfmuooEni6JPG1BFe7oLk5DZRo/ImydXijoZl/4kh2406boRQ7lxTYzbZEEXEhj9NQ==
"@vue/compiler-dom@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz#233c660289ce289a48e8fe759b07b95f607cd98e"
integrity sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==
dependencies:
"@vue/compiler-core" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/compiler-core" "3.5.10"
"@vue/shared" "3.5.10"
"@vue/compiler-dom@^3.4.0":
version "3.4.21"
@ -644,19 +702,19 @@
"@vue/compiler-core" "3.4.21"
"@vue/shared" "3.4.21"
"@vue/compiler-sfc@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.35.tgz#16f87dd3bdab64cef14d3a6fcf53f8673e404071"
integrity sha512-xacnRS/h/FCsjsMfxBkzjoNxyxEyKyZfBch/P4vkLRvYJwe5ChXmZZrj8Dsed/752H2Q3JE8kYu9Uyha9J6PgA==
"@vue/compiler-sfc@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz#95e262a5ed836521a5aeee9492cc265ad3f1c787"
integrity sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==
dependencies:
"@babel/parser" "^7.24.7"
"@vue/compiler-core" "3.4.35"
"@vue/compiler-dom" "3.4.35"
"@vue/compiler-ssr" "3.4.35"
"@vue/shared" "3.4.35"
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.10"
"@vue/compiler-dom" "3.5.10"
"@vue/compiler-ssr" "3.5.10"
"@vue/shared" "3.5.10"
estree-walker "^2.0.2"
magic-string "^0.30.10"
postcss "^8.4.40"
magic-string "^0.30.11"
postcss "^8.4.47"
source-map-js "^1.2.0"
"@vue/compiler-sfc@^3.2.47":
@ -683,13 +741,13 @@
"@vue/compiler-dom" "3.2.47"
"@vue/shared" "3.2.47"
"@vue/compiler-ssr@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.35.tgz#0774c9a0afed74d71615209904b38f3fa9711adb"
integrity sha512-7iynB+0KB1AAJKk/biENTV5cRGHRdbdaD7Mx3nWcm1W8bVD6QmnH3B4AHhQQ1qZHhqFwzEzMwiytXm3PX1e60A==
"@vue/compiler-ssr@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz#195f83ae7c52174be37fd7a4a0217132c1c0ed11"
integrity sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==
dependencies:
"@vue/compiler-dom" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/compiler-dom" "3.5.10"
"@vue/shared" "3.5.10"
"@vue/compiler-vue2@^2.7.16":
version "2.7.16"
@ -700,15 +758,15 @@
he "^1.2.0"
"@vue/devtools-api@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
"@vue/devtools-api@^6.6.3":
version "6.6.3"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.3.tgz#b23a588154cba8986bba82b6e1d0248bde3fd1a0"
integrity sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==
"@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/eslint-config-typescript@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz#f5f3d986ace34a10f403921d5044831b89a1b679"
@ -718,12 +776,12 @@
"@typescript-eslint/parser" "^7.1.1"
vue-eslint-parser "^9.3.1"
"@vue/language-core@2.0.29":
version "2.0.29"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.29.tgz#19462d786cd7a1c21dbe575b46970a57094e0357"
integrity sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==
"@vue/language-core@2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.1.6.tgz#b48186bdb9b3ef2b83e1f76d5b1ac357b3a7ed94"
integrity sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==
dependencies:
"@volar/language-core" "~2.4.0-alpha.18"
"@volar/language-core" "~2.4.1"
"@vue/compiler-dom" "^3.4.0"
"@vue/compiler-vue2" "^2.7.16"
"@vue/shared" "^3.4.0"
@ -743,38 +801,38 @@
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.35.tgz#dfbb4f5371da1290ac86e3313d0e9a68bb0ab38d"
integrity sha512-Ggtz7ZZHakriKioveJtPlStYardwQH6VCs9V13/4qjHSQb/teE30LVJNrbBVs4+aoYGtTQKJbTe4CWGxVZrvEw==
"@vue/reactivity@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.10.tgz#81140ef0b05096973356d3c8fc32f48c79940b9c"
integrity sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==
dependencies:
"@vue/shared" "3.4.35"
"@vue/shared" "3.5.10"
"@vue/runtime-core@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.35.tgz#c036013a7b1bbe0d14a6b76eb4355dae6690d2e6"
integrity sha512-D+BAjFoWwT5wtITpSxwqfWZiBClhBbR+bm0VQlWYFOadUUXFo+5wbe9ErXhLvwguPiLZdEF13QAWi2vP3ZD5tA==
"@vue/runtime-core@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.10.tgz#e902eb2640fa6ab4cc4589af263818a898812668"
integrity sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==
dependencies:
"@vue/reactivity" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/reactivity" "3.5.10"
"@vue/shared" "3.5.10"
"@vue/runtime-dom@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.35.tgz#74254c7c327163d692e1d7d2b6d9e92463744e90"
integrity sha512-yGOlbos+MVhlS5NWBF2HDNgblG8e2MY3+GigHEyR/dREAluvI5tuUUgie3/9XeqhPE4LF0i2wjlduh5thnfOqw==
"@vue/runtime-dom@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz#dca26d7761147373c6929f1370cf2733aa19f3de"
integrity sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==
dependencies:
"@vue/reactivity" "3.4.35"
"@vue/runtime-core" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/reactivity" "3.5.10"
"@vue/runtime-core" "3.5.10"
"@vue/shared" "3.5.10"
csstype "^3.1.3"
"@vue/server-renderer@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.35.tgz#188e94e82d8e729ba7b40dd91d10678b85f77c6b"
integrity sha512-iZ0e/u9mRE4T8tNhlo0tbA+gzVkgv8r5BX6s1kRbOZqfpq14qoIvCZ5gIgraOmYkMYrSEZgkkojFPr+Nyq/Mnw==
"@vue/server-renderer@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.10.tgz#90462492c30c8cae499b9149d1b90af2ebfe7599"
integrity sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==
dependencies:
"@vue/compiler-ssr" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/compiler-ssr" "3.5.10"
"@vue/shared" "3.5.10"
"@vue/shared@3.2.47":
version "3.2.47"
@ -786,10 +844,10 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1"
integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==
"@vue/shared@3.4.35":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.35.tgz#5432f4b1c79e763fcf78cc830faf59ff01248968"
integrity sha512-hvuhBYYDe+b1G8KHxsQ0diDqDMA8D9laxWZhNAjE83VZb5UDaXl9Xnz7cGdDSyiHM90qqI/CyGMcpBpiDy6VVQ==
"@vue/shared@3.5.10":
version "3.5.10"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.10.tgz#066f7dde31e09d700123e92e63eaa126cda21a17"
integrity sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==
"@vue/tsconfig@^0.5.1":
version "0.5.1"
@ -1172,17 +1230,17 @@ escodegen@^2.1.0:
optionalDependencies:
source-map "~0.6.1"
eslint-plugin-vue@^9.27.0:
version "9.27.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz#c22dae704a03d9ecefa81364ff89f60ce0481f94"
integrity sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==
eslint-plugin-vue@^9.28.0:
version "9.28.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz#e4412f0c1024bafd15ffeaa6f76f4c99152e2765"
integrity sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
globals "^13.24.0"
natural-compare "^1.4.0"
nth-check "^2.1.1"
postcss-selector-parser "^6.0.15"
semver "^7.6.0"
semver "^7.6.3"
vue-eslint-parser "^9.4.3"
xml-name-validator "^4.0.0"
@ -1222,19 +1280,23 @@ eslint-visitor-keys@^4.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
eslint@^9.8.0:
version "9.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.8.0.tgz#a4f4a090c8ea2d10864d89a6603e02ce9f649f0f"
integrity sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==
eslint@^9.11.1:
version "9.11.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.11.1.tgz#701e5fc528990153f9cef696d8427003b5206567"
integrity sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.11.0"
"@eslint/config-array" "^0.17.1"
"@eslint/config-array" "^0.18.0"
"@eslint/core" "^0.6.0"
"@eslint/eslintrc" "^3.1.0"
"@eslint/js" "9.8.0"
"@eslint/js" "9.11.1"
"@eslint/plugin-kit" "^0.2.0"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.3.0"
"@nodelib/fs.walk" "^1.2.8"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@ -1254,7 +1316,6 @@ eslint@^9.8.0:
is-glob "^4.0.0"
is-path-inside "^3.0.3"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
@ -1843,12 +1904,12 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.30.10:
version "0.30.10"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
magic-string@^0.30.11:
version "0.30.11"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954"
integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
"@jridgewell/sourcemap-codec" "^1.5.0"
memorystream@^0.3.1:
version "0.3.1"
@ -2072,10 +2133,10 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picocolors@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
picocolors@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
@ -2118,23 +2179,14 @@ postcss@^8.1.10:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.39:
version "8.4.39"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
postcss@^8.4.43, postcss@^8.4.47:
version "8.4.47"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.1"
source-map-js "^1.2.0"
postcss@^8.4.40:
version "8.4.40"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8"
integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.1"
source-map-js "^1.2.0"
picocolors "^1.1.0"
source-map-js "^1.2.1"
prelude-ls@^1.2.1:
version "1.2.1"
@ -2205,26 +2257,29 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rollup@^4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a"
integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==
rollup@^4.20.0:
version "4.21.2"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7"
integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.13.0"
"@rollup/rollup-android-arm64" "4.13.0"
"@rollup/rollup-darwin-arm64" "4.13.0"
"@rollup/rollup-darwin-x64" "4.13.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.13.0"
"@rollup/rollup-linux-arm64-gnu" "4.13.0"
"@rollup/rollup-linux-arm64-musl" "4.13.0"
"@rollup/rollup-linux-riscv64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-musl" "4.13.0"
"@rollup/rollup-win32-arm64-msvc" "4.13.0"
"@rollup/rollup-win32-ia32-msvc" "4.13.0"
"@rollup/rollup-win32-x64-msvc" "4.13.0"
"@rollup/rollup-android-arm-eabi" "4.21.2"
"@rollup/rollup-android-arm64" "4.21.2"
"@rollup/rollup-darwin-arm64" "4.21.2"
"@rollup/rollup-darwin-x64" "4.21.2"
"@rollup/rollup-linux-arm-gnueabihf" "4.21.2"
"@rollup/rollup-linux-arm-musleabihf" "4.21.2"
"@rollup/rollup-linux-arm64-gnu" "4.21.2"
"@rollup/rollup-linux-arm64-musl" "4.21.2"
"@rollup/rollup-linux-powerpc64le-gnu" "4.21.2"
"@rollup/rollup-linux-riscv64-gnu" "4.21.2"
"@rollup/rollup-linux-s390x-gnu" "4.21.2"
"@rollup/rollup-linux-x64-gnu" "4.21.2"
"@rollup/rollup-linux-x64-musl" "4.21.2"
"@rollup/rollup-win32-arm64-msvc" "4.21.2"
"@rollup/rollup-win32-ia32-msvc" "4.21.2"
"@rollup/rollup-win32-x64-msvc" "4.21.2"
fsevents "~2.3.2"
run-parallel@^1.1.9:
@ -2271,12 +2326,10 @@ semver@^7.3.6:
dependencies:
lru-cache "^6.0.0"
semver@^7.6.0:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
semver@^7.6.3:
version "7.6.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
shebang-command@^1.2.0:
version "1.2.0"
@ -2321,10 +2374,10 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
sortablejs@^1.15.2:
version "1.15.2"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.2.tgz#4e9f7bda4718bd1838add9f1866ec77169149809"
integrity sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==
sortablejs@^1.15.3:
version "1.15.3"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.3.tgz#033668db5ebfb11167d1249ab88e748f27959e29"
integrity sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
@ -2336,6 +2389,11 @@ source-map-js@^1.2.0:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@ -2448,10 +2506,10 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
terser@^5.31.3:
version "5.31.3"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38"
integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==
terser@^5.34.0:
version "5.34.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.0.tgz#62f2496542290bc6d8bf886edaee7fac158e37e4"
integrity sha512-y5NUX+U9HhVsK/zihZwoq4r9dICLyV2jXGOriDAVOeKhq3LKVjgJbGO90FisozXLlJfvjHqgckGmJFBb9KYoWQ==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
@ -2463,6 +2521,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@ -2487,10 +2550,10 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^5.5.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.6.2:
version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
ufo@^1.1.2:
version "1.1.2"
@ -2507,10 +2570,10 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
undici-types@~6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.13.0.tgz#e3e79220ab8c81ed1496b5812471afd7cf075ea5"
integrity sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==
undici-types@~6.19.2:
version "6.19.6"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.6.tgz#e218c3df0987f4c0e0008ca00d6b6472d9b89b36"
integrity sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==
universalify@^2.0.0:
version "2.0.0"
@ -2556,19 +2619,19 @@ vite-plugin-compression@^0.5.1:
debug "^4.3.3"
fs-extra "^10.0.0"
vite-plugin-css-injected-by-js@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.1.tgz#b9c568c21b131d08e31aa6d368ee39c9d6c1b6c1"
integrity sha512-9ioqwDuEBxW55gNoWFEDhfLTrVKXEEZgl5adhWmmqa88EQGKfTmexy4v1Rh0pAS6RhKQs2bUYQArprB32JpUZQ==
vite-plugin-css-injected-by-js@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz#1f75d16ad5c05b6b49bf18018099a189ec2e46ad"
integrity sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ==
vite@^5.3.5:
version "5.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.5.tgz#b847f846fb2b6cb6f6f4ed50a830186138cb83d8"
integrity sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==
vite@^5.4.8:
version "5.4.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.39"
rollup "^4.13.0"
postcss "^8.4.43"
rollup "^4.20.0"
optionalDependencies:
fsevents "~2.3.3"
@ -2603,7 +2666,7 @@ vue-eslint-parser@^9.4.3:
lodash "^4.17.21"
semver "^7.3.6"
vue-i18n@^9.13.1:
vue-i18n@9.13.1:
version "9.13.1"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.13.1.tgz#a292c8021b7be604ebfca5609ae1f8fafe5c36d7"
integrity sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==
@ -2612,32 +2675,32 @@ vue-i18n@^9.13.1:
"@intlify/shared" "9.13.1"
"@vue/devtools-api" "^6.5.0"
vue-router@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.4.2.tgz#bc7bf27f108fc15e5cc2a30b314a662275e2b2bb"
integrity sha512-1qNybkn2L7QsLzaXs8nvlQmRKp8XF8DCxZys/Jr1JpQcHsKUxTKzTxCVA1G7NfBfwRIBgCJPoujOG5lHCCNUxw==
vue-router@^4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.4.5.tgz#bdf535e4cf32414ebdea6b4b403593efdb541388"
integrity sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==
dependencies:
"@vue/devtools-api" "^6.6.3"
"@vue/devtools-api" "^6.6.4"
vue-tsc@^2.0.29:
version "2.0.29"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.29.tgz#bf7e9605af9fadec7fd6037d242217f5c6ad2c3b"
integrity sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==
vue-tsc@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.1.6.tgz#d93fdc617da6546674301a746fd7089ea6d4543d"
integrity sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==
dependencies:
"@volar/typescript" "~2.4.0-alpha.18"
"@vue/language-core" "2.0.29"
"@volar/typescript" "~2.4.1"
"@vue/language-core" "2.1.6"
semver "^7.5.4"
vue@^3.4.35:
version "3.4.35"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.35.tgz#9ad23525919eece40153fdf8675d07ddd879eb33"
integrity sha512-+fl/GLmI4GPileHftVlCdB7fUL4aziPcqTudpTGXCT8s+iZWuOCeNEB5haX6Uz2IpRrbEXOgIFbe+XciCuGbNQ==
vue@^3.5.10:
version "3.5.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.10.tgz#14be9d4655e07be8d5e8295d017815ed14337f96"
integrity sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==
dependencies:
"@vue/compiler-dom" "3.4.35"
"@vue/compiler-sfc" "3.4.35"
"@vue/runtime-dom" "3.4.35"
"@vue/server-renderer" "3.4.35"
"@vue/shared" "3.4.35"
"@vue/compiler-dom" "3.5.10"
"@vue/compiler-sfc" "3.5.10"
"@vue/runtime-dom" "3.5.10"
"@vue/server-renderer" "3.5.10"
"@vue/shared" "3.5.10"
webpack-sources@^3.2.3:
version "3.2.3"

Binary file not shown.