Merge branch 'tbnobody:master' into AlarmMessageType

This commit is contained in:
mattreim 2024-10-07 14:45:07 +02:00 committed by GitHub
commit d07f91c0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
94 changed files with 2729 additions and 1183 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-Compiled 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

@ -40,41 +40,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,122 @@
"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
}
},
{
"name": "OpenDTU Fusion v2 PoE with SH1106 Display",
"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": 3,
"data": 2,
"clk": 1
}
},
{
"name": "OpenDTU Fusion v2 PoE with SSD1306 Display",
"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": 2,
"data": 2,
"clk": 1
}
}
]

View File

@ -6,29 +6,42 @@
#include <TaskSchedulerDeclarations.h>
// mqtt discovery device classes
enum {
enum DeviceClassType {
DEVICE_CLS_NONE = 0,
DEVICE_CLS_CURRENT,
DEVICE_CLS_ENERGY,
DEVICE_CLS_PWR,
DEVICE_CLS_VOLTAGE,
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", "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[] = {
@ -41,7 +54,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
{ FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT },
{ FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT },
{ FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE },
@ -58,13 +71,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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_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 StateClassType state_class, 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,18 +24,18 @@ 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;
typedef struct DtuNetworkEventCbList {
DtuNetworkEventCb cb;
network_event event;
NetworkEventCbList()
DtuNetworkEventCbList()
: cb(nullptr)
, event(network_event::NETWORK_UNKNOWN)
{
}
} NetworkEventCbList_t;
} DtuNetworkEventCbList_t;
class NetworkSettingsClass {
public:
@ -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;
@ -81,8 +82,9 @@ private:
bool _dnsServerStatus = false;
network_mode _networkMode = network_mode::Undefined;
bool _ethConnected = false;
std::vector<NetworkEventCbList_t> _cbEventList;
std::vector<DtuNetworkEventCbList_t> _cbEventList;
bool _lastMdnsEnabled = false;
std::unique_ptr<W5500> _w5500;
};
extern NetworkSettingsClass NetworkSettings;

View File

@ -12,6 +12,7 @@
struct PinMapping_t {
char name[MAPPING_NAME_STRLEN + 1];
int8_t nrf24_miso;
int8_t nrf24_mosi;
int8_t nrf24_clk;
@ -26,6 +27,14 @@ 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;
#if CONFIG_ETH_USE_ESP32_EMAC
int8_t eth_phy_addr;
bool eth_enabled;
int eth_power;
@ -33,11 +42,14 @@ struct PinMapping_t {
int eth_mdio;
eth_phy_type_t eth_type;
eth_clock_mode_t eth_clk_mode;
#endif
uint8_t display_type;
uint8_t display_data;
uint8_t display_clk;
uint8_t display_cs;
uint8_t display_reset;
int8_t led[PINMAPPING_LED_COUNT];
};
@ -49,7 +61,10 @@ public:
bool isValidNrf24Config() const;
bool isValidCmt2300Config() const;
bool isValidW5500Config() const;
#if CONFIG_ETH_USE_ESP32_EMAC
bool isValidEthConfig() const;
#endif
private:
PinMapping_t _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();
};

29
include/W5500.h Normal file
View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_eth.h> // required for esp_eth_handle_t
#include <esp_netif.h>
#include <memory>
class W5500 {
private:
explicit W5500(spi_device_handle_t spi, gpio_num_t pin_int);
public:
W5500(const W5500&) = delete;
W5500& operator=(const W5500&) = delete;
~W5500();
static std::unique_ptr<W5500> setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst);
String macAddress();
private:
static bool connection_check_spi(spi_device_handle_t spi);
static bool connection_check_interrupt(gpio_num_t pin_int);
esp_eth_handle_t eth_handle;
esp_netif_t* eth_netif;
};

View File

@ -30,6 +30,7 @@ class WebApiClass {
public:
WebApiClass();
void init(Scheduler& scheduler);
void reload();
static bool checkCredentials(AsyncWebServerRequest* request);
static bool checkCredentialsReadonly(AsyncWebServerRequest* request);

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

@ -8,9 +8,11 @@ class WebApiWsConsoleClass {
public:
WebApiWsConsoleClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private:
AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth;
Task _wsCleanupTask;
void wsCleanupTaskCb();

View File

@ -11,6 +11,7 @@ class WebApiWsLiveClass {
public:
WebApiWsLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private:
static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
@ -24,6 +25,7 @@ private:
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth;
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };

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

@ -34,7 +34,7 @@ uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const
uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const
{
if ((frequency % getChannelWidth()) != 0) {
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth());
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %" PRId32 " kHz!\r\n", frequency / 1000000.0, getChannelWidth());
return 0xFF; // ERROR
}
if (frequency < getMinFrequency() || frequency > getMaxFrequency()) {
@ -43,7 +43,7 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) con
return 0xFF; // ERROR
}
if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) {
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n",
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%" PRId32 " - %" PRId32 " MHz)\r\n",
frequency / 1000000.0,
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6),
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6));
@ -167,9 +167,9 @@ void HoymilesRadio_CMT::loop()
// Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len);
inv->addRxFragment(f.fragment, f.len, f.rssi);
} else {
Hoymiles.getMessageOutput()->println("Inverter Not found!");
}
@ -194,9 +194,9 @@ void HoymilesRadio_CMT::setPALevel(const int8_t paLevel)
}
if (_radio->setPALevel(paLevel)) {
Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel);
Hoymiles.getMessageOutput()->printf("CMT TX power set to %" PRId8 " dBm\r\n", paLevel);
} else {
Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel);
Hoymiles.getMessageOutput()->printf("CMT TX power %" PRId8 " dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel);
}
}

View File

@ -76,11 +76,11 @@ void HoymilesRadio_NRF::loop()
if (nullptr != inv) {
// Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel);
Hoymiles.getMessageOutput()->printf("RX Channel: %" PRId8 " --> ", f.channel);
dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len);
inv->addRxFragment(f.fragment, f.len, f.rssi);
} else {
Hoymiles.getMessageOutput()->println("Inverter Not found!");
}
@ -183,7 +183,7 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd)
openWritingPipe(s);
_radio->setRetries(3, 15);
Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ",
Hoymiles.getMessageOutput()->printf("TX %s Channel: %" PRId8 " --> ",
cmd.getCommandName().c_str(), _radio->getChannel());
cmd.dumpDataPayload(Hoymiles.getMessageOutput());
_radio->write(cmd.getDataPayload(), cmd.getDataSize());

View File

@ -48,7 +48,7 @@ bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const u
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount();
if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n",
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize);
return false;

View File

@ -48,7 +48,7 @@ bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount();
if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n",
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize);
return false;

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

@ -137,6 +137,11 @@ bool InverterAbstract::getClearEventlogOnMidnight() const
return _clearEventlogOnMidnight;
}
int8_t InverterAbstract::getLastRssi() const
{
return _lastRssi;
}
bool InverterAbstract::sendChangeChannelRequest()
{
return false;
@ -185,8 +190,10 @@ void InverterAbstract::clearRxFragmentBuffer()
_rxFragmentRetransmitCnt = 0;
}
void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len)
void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi)
{
_lastRssi = rssi;
if (len < 11) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__);
return;
@ -208,7 +215,7 @@ void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len
}
if (fragmentId >= MAX_RF_FRAGMENT_COUNT) {
Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId);
Hoymiles.getMessageOutput()->printf("ERROR: fragment id %" PRId8 " is too large for buffer and ignored\r\n", fragmentId);
return;
}
@ -272,3 +279,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

@ -61,10 +61,36 @@ public:
void setClearEventlogOnMidnight(const bool enabled);
bool getClearEventlogOnMidnight() const;
int8_t getLastRssi() const;
void clearRxFragmentBuffer();
void addRxFragment(const uint8_t fragment[], const uint8_t len);
void addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi);
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;
@ -107,6 +133,8 @@ private:
bool _zeroYieldDayOnMidnight = false;
bool _clearEventlogOnMidnight = false;
int8_t _lastRssi = -127;
std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser;
std::unique_ptr<GridProfileParser> _gridProfileParser;

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_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,6 +121,10 @@ 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)

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.12
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_poe]
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

@ -87,7 +87,7 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos
if (maxWatts > 999) {
snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000);
} else {
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts));
snprintf(fmtText, sizeof(fmtText), "%" PRId16 "W", static_cast<uint16_t>(maxWatts));
}
if (isFullscreen) {

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,46 @@ 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, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Temperature", "dtu/temperature", "°C", "", DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
yield();
publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, 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, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, STATE_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", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, 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", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, 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, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_NONE);
publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RSSI", "radio/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) {
@ -94,8 +111,6 @@ void MqttHandleHassClass::publishConfig()
}
}
}
yield();
}
}
@ -128,8 +143,6 @@ 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];
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, fieldType.stateClsId, 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 StateClassType state_class, const CategoryType category)
{
const String serial = inv->serialString();
String buttonId = caption;
String buttonId = name;
buttonId.replace(" ", "_");
buttonId.toLowerCase();
@ -185,41 +186,30 @@ 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, state_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 StateClassType state_class, const CategoryType category)
{
const String serial = inv->serialString();
String buttonId = caption;
String buttonId = name;
buttonId.replace(" ", "_");
buttonId.toLowerCase();
@ -227,150 +217,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, state_class, 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 +295,129 @@ 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 StateClassType state_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 (state_class != STATE_CLS_NONE) {
doc["stat_cla"] = stateClass_name[state_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 StateClassType state_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, state_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 StateClassType state_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, state_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 StateClassType state_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, state_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 StateClassType state_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, state_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 StateClassType state_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, state_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 StateClassType state_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, state_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,15 @@ 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));
MqttSettings.publish(subtopic + "/radio/rssi", String(inv->getLastRssi()));
if (inv->DevInfo()->getLastUpdate() > 0) {
// Bootloader Version
MqttSettings.publish(subtopic + "/device/bootloaderversion", String(inv->DevInfo()->getFwBootloaderVersion()));
@ -146,7 +148,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 +156,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 +173,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 +204,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 +214,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));
MessageOutput.printf("Set inverter power to: %" PRId32 "\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 +230,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,41 @@ 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 = W5500::setup(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst);
if (_w5500)
MessageOutput.println("W5500: Connection successful");
else
MessageOutput.println("W5500: Connection error!!");
}
#if CONFIG_ETH_USE_ESP32_EMAC
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
}
#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 +97,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: %" PRId8 "\r\n", info.wifi_sta_disconnected.reason);
if (_networkMode == network_mode::WiFi) {
MessageOutput.println("Try reconnecting");
WiFi.disconnect(true, false);
@ -95,12 +117,12 @@ 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;
}
NetworkEventCbList_t newEventHandler;
DtuNetworkEventCbList_t newEventHandler;
newEventHandler.cb = cbEvent;
newEventHandler.event = event;
_cbEventList.push_back(newEventHandler);
@ -109,8 +131,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 +188,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()
@ -210,7 +226,7 @@ void NetworkSettingsClass::loop()
if (_adminEnabled && _adminTimeoutCounterMax > 0) {
_adminTimeoutCounter++;
if (_adminTimeoutCounter % 10 == 0) {
MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax);
MessageOutput.printf("Admin AP remaining seconds: %" PRId32 " / %" PRId32 "\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax);
}
}
_connectTimeoutTimer++;
@ -399,6 +415,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,58 @@
#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
#if CONFIG_ETH_USE_ESP32_EMAC
#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
#endif
PinMappingClass PinMapping;
PinMappingClass::PinMappingClass()
@ -103,18 +155,26 @@ 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;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = true;
#else
_pinMapping.eth_enabled = false;
#endif
_pinMapping.eth_phy_addr = ETH_PHY_ADDR;
_pinMapping.eth_power = ETH_PHY_POWER;
_pinMapping.eth_mdc = ETH_PHY_MDC;
_pinMapping.eth_mdio = ETH_PHY_MDIO;
_pinMapping.eth_type = ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = ETH_CLK_MODE;
#endif
_pinMapping.display_type = DISPLAY_TYPE;
_pinMapping.display_data = DISPLAY_DATA;
@ -164,18 +224,26 @@ 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;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true;
#else
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false;
#endif
_pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR;
_pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER;
_pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC;
_pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO;
_pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE;
#endif
_pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE;
_pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA;
@ -211,7 +279,21 @@ 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;
}
#if CONFIG_ETH_USE_ESP32_EMAC
bool PinMappingClass::isValidEthConfig() const
{
return _pinMapping.eth_enabled;
return _pinMapping.eth_enabled
&& _pinMapping.eth_mdc >= 0
&& _pinMapping.eth_mdio >= 0;
}
#endif

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,20 +57,10 @@ 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()) {
MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line);
MessageOutput.printf("Alloc failed: %s, %" PRId16 "\r\n", function, line);
return false;
}

153
src/W5500.cpp Normal file
View File

@ -0,0 +1,153 @@
// 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(spi_device_handle_t spi, gpio_num_t pin_int)
: eth_handle(nullptr)
, eth_netif(nullptr)
{
// 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?
}
std::unique_ptr<W5500> W5500::setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst)
{
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));
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)
return nullptr;
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
if (!connection_check_spi(spi))
return nullptr;
if (!connection_check_interrupt(static_cast<gpio_num_t>(pin_int)))
return nullptr;
// Use Arduino functions to temporarily attach interrupt to enable the GPIO ISR service
// (if we used ESP-IDF functions, a warning would be printed the first time anyone uses attachInterrupt)
attachInterrupt(pin_int, nullptr, FALLING);
detachInterrupt(pin_int);
// Return to default state once again after connection check and temporary interrupt registration
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
return std::unique_ptr<W5500>(new W5500(spi, static_cast<gpio_num_t>(pin_int)));
}
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);
}
bool W5500::connection_check_spi(spi_device_handle_t spi)
{
spi_transaction_t trans = {
.flags = SPI_TRANS_USE_RXDATA,
.cmd = 0x0039, // actually address (VERSIONR)
.addr = (0b00000 << 3) | (0 << 2) | (0b00 < 0), // actually command (common register, read, VDM)
.length = 8,
.rxlength = 8,
.user = nullptr,
.tx_buffer = nullptr,
.rx_data = {},
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
// Version number (VERSIONR) is always 0x04
return *reinterpret_cast<uint8_t*>(&trans.rx_data) == 0x04;
}
bool W5500::connection_check_interrupt(gpio_num_t pin_int)
{
gpio_set_direction(pin_int, GPIO_MODE_INPUT);
gpio_set_pull_mode(pin_int, GPIO_PULLDOWN_ONLY);
int level = gpio_get_level(pin_int);
// Interrupt line must be high
return level == 1;
}

View File

@ -39,6 +39,12 @@ void WebApiClass::init(Scheduler& scheduler)
_server.begin();
}
void WebApiClass::reload()
{
_webApiWsConsole.reload();
_webApiWsLive.reload();
}
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
{
CONFIG_T& config = Configuration.get();
@ -131,7 +137,7 @@ bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResp
root["code"] = WebApiError::GenericInternalServerError;
root["type"] = "danger";
response->setCode(500);
MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line);
MessageOutput.printf("WebResponse failed: %s, %" PRId16 "\r\n", function, line);
ret_val = false;
}

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,15 @@ 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;
#if CONFIG_ETH_USE_ESP32_EMAC
auto ethPinObj = curPin["eth"].to<JsonObject>();
ethPinObj["enabled"] = pin.eth_enabled;
ethPinObj["phy_addr"] = pin.eth_phy_addr;
@ -58,6 +67,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
ethPinObj["mdio"] = pin.eth_mdio;
ethPinObj["type"] = pin.eth_type;
ethPinObj["clk_mode"] = pin.eth_clk_mode;
#endif
auto displayPinObj = curPin["display"].to<JsonObject>();
displayPinObj["type"] = pin.display_type;
@ -103,8 +113,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 +159,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
if (performRestart) {
Utils::restartDtu();
RestartHelper.triggerRestart();
}
}

View File

@ -49,7 +49,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
// DTU Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
snprintf(buffer, sizeof(buffer), "%0x%08x",
snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32,
((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)),
((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF)));
root["serial"] = buffer;
@ -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)
@ -44,7 +45,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
// Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
snprintf(buffer, sizeof(buffer), "%0x%08x",
snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32,
((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)),
((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF)));
obj["serial"] = buffer;
@ -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

@ -42,23 +42,23 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_heap_size System memory size\n");
stream->print("# TYPE opendtu_heap_size gauge\n");
stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize());
stream->printf("opendtu_heap_size %" PRId32 "\n", ESP.getHeapSize());
stream->print("# HELP opendtu_free_heap_size System free memory\n");
stream->print("# TYPE opendtu_free_heap_size gauge\n");
stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap());
stream->printf("opendtu_free_heap_size %" PRId32 "\n", ESP.getFreeHeap());
stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n");
stream->print("# TYPE opendtu_biggest_heap_block gauge\n");
stream->printf("opendtu_biggest_heap_block %zu\n", ESP.getMaxAllocHeap());
stream->printf("opendtu_biggest_heap_block %" PRId32 "\n", ESP.getMaxAllocHeap());
stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n");
stream->print("# TYPE opendtu_heap_min_free gauge\n");
stream->printf("opendtu_heap_min_free %zu\n", ESP.getMinFreeHeap());
stream->printf("opendtu_heap_min_free %" PRId32 "\n", ESP.getMinFreeHeap());
stream->print("# HELP wifi_rssi WiFi RSSI\n");
stream->print("# TYPE wifi_rssi gauge\n");
stream->printf("wifi_rssi %d\n", WiFi.RSSI());
stream->printf("wifi_rssi %" PRId8 "\n", WiFi.RSSI());
stream->print("# HELP wifi_station WiFi Station info\n");
stream->print("# TYPE wifi_station gauge\n");
@ -73,14 +73,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_last_update last update from inverter in s\n");
stream->print("# TYPE opendtu_last_update gauge\n");
}
stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n",
stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %" PRId32 "\n",
serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000);
if (i == 0) {
stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_relative gauge\n");
}
stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n",
stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0);
if (inv->DevInfo()->getMaxPower() > 0) {
@ -88,7 +88,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n");
}
stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n",
stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0);
}
@ -126,7 +126,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String&
stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId));
stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName);
}
stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n",
stream->printf("opendtu_%s{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n",
chanName,
serial.c_str(),
idx,
@ -150,7 +150,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_PanelInfo panel information\n");
stream->print("# TYPE opendtu_PanelInfo gauge\n");
}
stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n",
stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n",
serial.c_str(),
idx,
inv->name(),
@ -161,7 +161,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_MaxPower panel maximum output power\n");
stream->print("# TYPE opendtu_MaxPower gauge\n");
}
stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %d\n",
stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\"} %d\n",
serial.c_str(),
idx,
inv->name(),
@ -172,7 +172,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n");
stream->print("# TYPE opendtu_YieldTotalOffset gauge\n");
}
stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n",
stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%" PRId16 "\"} %f\n",
serial.c_str(),
idx,
inv->name(),

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__);
@ -71,6 +71,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
WebApi.writeConfig(retMsg);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
WebApi.reload();
}
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)

View File

@ -21,16 +21,30 @@ void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler)
scheduler.addTask(_wsCleanupTask);
_wsCleanupTask.enable();
_simpleDigestAuth.setUsername(AUTH_USERNAME);
_simpleDigestAuth.setRealm("console websocket");
reload();
}
void WebApiWsConsoleClass::reload()
{
_ws.removeMiddleware(&_simpleDigestAuth);
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { return; }
_ws.enable(false);
_simpleDigestAuth.setPassword(config.Security.Password);
_ws.addMiddleware(&_simpleDigestAuth);
_ws.closeAll();
_ws.enable(true);
}
void WebApiWsConsoleClass::wsCleanupTaskCb()
{
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
_ws.cleanupClients();
if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", "");
} else {
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
}
}

View File

@ -36,18 +36,31 @@ void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
scheduler.addTask(_sendDataTask);
_sendDataTask.enable();
_simpleDigestAuth.setUsername(AUTH_USERNAME);
_simpleDigestAuth.setRealm("live websocket");
reload();
}
void WebApiWsLiveClass::reload()
{
_ws.removeMiddleware(&_simpleDigestAuth);
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { return; }
_ws.enable(false);
_simpleDigestAuth.setPassword(config.Security.Password);
_ws.addMiddleware(&_simpleDigestAuth);
_ws.closeAll();
_ws.enable(true);
}
void WebApiWsLiveClass::wsCleanupTaskCb()
{
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
_ws.cleanupClients();
if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", "");
} else {
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
}
}
void WebApiWsLiveClass::sendDataTaskCb()
@ -134,6 +147,13 @@ 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;
root["radio_stats"]["rssi"] = inv->getLastRssi();
}
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
@ -143,7 +153,7 @@ void setup()
if (config.Dtu.Serial == DTU_SERIAL) {
MessageOutput.print("generate serial based on ESP chip id: ");
const uint64_t dtuId = Utils::generateDtuSerial();
MessageOutput.printf("%0x%08x... ",
MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
((uint32_t)(dtuId & 0xFFFFFFFF)));
config.Dtu.Serial = dtuId;
@ -154,6 +164,7 @@ void setup()
InverterSettings.init(scheduler);
Datastore.init(scheduler);
RestartHelper.init(scheduler);
}
void loop()

View File

@ -1,22 +1,12 @@
/* eslint-env node */
import path from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import pluginVue from 'eslint-plugin-vue'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
import vueTsEslintConfig from '@vue/eslint-config-typescript'
export default [
js.configs.recommended,
...pluginVue.configs['flat/essential'],
...compat.extends("@vue/eslint-config-typescript/recommended"),
...vueTsEslintConfig(),
{
files: [
"**/*.vue",

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.11",
"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",
"@vue/eslint-config-typescript": "^13.0.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-typescript": "^14.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.8.0",
"eslint-plugin-vue": "^9.27.0",
"eslint": "^9.12.0",
"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.1",
"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>
<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

@ -216,7 +216,9 @@ export default defineComponent({
this.$router.push('/');
},
onClick() {
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove('show');
if (this.$refs.navbarCollapse) {
(this.$refs.navbarCollapse as HTMLElement).classList.remove('show');
}
},
getEasterSunday(year: number): Date {
const f = Math.floor;

View File

@ -141,7 +141,19 @@
"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...",
"Rssi": "RSSI des zuletzt empfangenen Paketes",
"RssiHint": "HM-Wechselrichter unterstützen nur RSSI-Werte < -64 dBm und > -64 dBm. In diesem Fall wird -80 dBm und -30 dBm angezeigt.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Beginn",

View File

@ -141,7 +141,19 @@
"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...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Start",

View File

@ -141,7 +141,19 @@
"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...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Départ",

View File

@ -21,6 +21,16 @@ 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;
rssi: number;
}
export interface Inverter {
serial: string;
name: string;
@ -35,6 +45,7 @@ export interface Inverter {
AC: InverterStatistics[];
DC: InverterStatistics[];
INV: InverterStatistics[];
radio_stats: RadioStatistics;
}
export interface Total {

View File

@ -48,4 +48,4 @@ export interface Device {
display: Display;
}
export interface PinMapping extends Array<Device> {}
export type PinMapping = Array<Device>;

View File

@ -84,7 +84,7 @@ export default defineComponent({
this.socket.onmessage = (event) => {
console.log(event);
let outstr = new String(event.data);
let outstr = String(event.data);
let removedNewline = false;
if (outstr.endsWith('\n')) {
outstr = outstr.substring(0, outstr.length - 1);
@ -108,7 +108,9 @@ export default defineComponent({
},
// Send heartbeat packets regularly * 59s Send a heartbeat
heartCheck() {
this.heartInterval && clearTimeout(this.heartInterval);
if (this.heartInterval) {
clearTimeout(this.heartInterval);
}
this.heartInterval = setInterval(() => {
if (this.socket.readyState === 1) {
// Connection status
@ -126,7 +128,9 @@ export default defineComponent({
// continue regardless of error
}
this.heartInterval && clearTimeout(this.heartInterval);
if (this.heartInterval) {
clearTimeout(this.heartInterval);
}
},
getOutDate(): string {
const u = new Date();

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>
@ -385,7 +385,7 @@ export default defineComponent({
return;
}
const srcId = this.getNumberFromLedId((event.target as Element).id);
this.deviceConfigList.led.every((v) => (v.brightness = this.deviceConfigList.led[srcId].brightness));
this.deviceConfigList.led.map((v) => (v.brightness = this.deviceConfigList.led[srcId].brightness));
},
},
});

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,120 @@
<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>
<tr>
<td>
{{ $t('home.Rssi') }}
<BIconInfoCircle v-tooltip :title="$t('home.RssiHint')" />
</td>
<td>
{{ $t('home.dBm', { dbm: $n(inverter.radio_stats.rssi) }) }}
</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,9 +505,11 @@ import { authHeader, authUrl, handleResponse, isLoggedIn } from '@/utils/authent
import * as bootstrap from 'bootstrap';
import {
BIconArrowCounterclockwise,
BIconBroadcast,
BIconCheckCircleFill,
BIconCpu,
BIconExclamationCircleFill,
BIconInfoCircle,
BIconJournalText,
BIconOutlet,
BIconPower,
@ -415,9 +532,11 @@ export default defineComponent({
InverterTotalInfo,
ModalDialog,
BIconArrowCounterclockwise,
BIconBroadcast,
BIconCheckCircleFill,
BIconCpu,
BIconExclamationCircleFill,
BIconInfoCircle,
BIconJournalText,
BIconOutlet,
BIconPower,
@ -461,6 +580,7 @@ export default defineComponent({
alertMessageLimit: '',
alertTypeLimit: 'info',
showAlertLimit: false,
performRadioStatsReset: false,
powerSettingView: {} as bootstrap.Modal,
powerSettingSerial: '',
@ -614,7 +734,9 @@ export default defineComponent({
},
// Send heartbeat packets regularly * 59s Send a heartbeat
heartCheck(duration: number = 59) {
this.heartInterval && clearTimeout(this.heartInterval);
if (this.heartInterval) {
clearTimeout(this.heartInterval);
}
this.heartInterval = setInterval(() => {
if (this.socket.readyState === 1) {
// Connection status
@ -627,7 +749,9 @@ export default defineComponent({
/** To break off websocket Connect */
closeSocket() {
this.socket.close();
this.heartInterval && clearTimeout(this.heartInterval);
if (this.heartInterval) {
clearTimeout(this.heartInterval);
}
this.isFirstFetchAfterConnect = true;
},
onShowEventlog(serial: string) {
@ -690,6 +814,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 +918,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();
}
});
},
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": [

View File

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

View File

@ -14,7 +14,7 @@ let proxy_target;
try {
// eslint-disable-next-line
proxy_target = require('./vite.user.ts').proxy_target;
} catch (error) {
} catch {
proxy_target = '192.168.20.110';
}
@ -45,6 +45,7 @@ export default defineConfig({
outDir: '../webapp_dist',
emptyOutDir: true,
minify: 'terser',
chunkSizeWarningLimit: 1024,
rollupOptions: {
output: {
// Only create one js file

File diff suppressed because it is too large Load Diff

Binary file not shown.