Merge branch 'master' into merge
This commit is contained in:
commit
4035a41027
13
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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,3 +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.
|
||||
required: true
|
||||
|
||||
17
.github/workflows/build.yml
vendored
17
.github/workflows/build.yml
vendored
@ -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 }}
|
||||
|
||||
@ -18,6 +18,12 @@
|
||||
"fix"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 🌎 Web Application",
|
||||
"labels": [
|
||||
"webapp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 📚 Documentation",
|
||||
"labels": [
|
||||
|
||||
2
.github/workflows/cpplint.yml
vendored
2
.github/workflows/cpplint.yml
vendored
@ -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:
|
||||
|
||||
16
.github/workflows/yarnlint.yml
vendored
16
.github/workflows/yarnlint.yml
vendored
@ -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
28
.github/workflows/yarnprettier.yml
vendored
Normal 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/
|
||||
39
README.md
39
README.md
@ -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/)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@ -22,6 +22,34 @@
|
||||
"clk_mode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WT32-ETH01 with SH1106",
|
||||
"links": [
|
||||
{"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"}
|
||||
],
|
||||
"nrf24": {
|
||||
"miso": 4,
|
||||
"mosi": 2,
|
||||
"clk": 32,
|
||||
"irq": 33,
|
||||
"en": 14,
|
||||
"cs": 15
|
||||
},
|
||||
"eth": {
|
||||
"enabled": true,
|
||||
"phy_addr": 1,
|
||||
"power": 16,
|
||||
"mdc": 23,
|
||||
"mdio": 18,
|
||||
"type": 0,
|
||||
"clk_mode": 0
|
||||
},
|
||||
"display": {
|
||||
"type": 3,
|
||||
"data": 5,
|
||||
"clk": 17
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WT32-ETH01 with SSD1306",
|
||||
"links": [
|
||||
@ -78,4 +106,4 @@
|
||||
"clk": 17
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
#define NTP_MAX_TIMEZONEDESCR_STRLEN 50
|
||||
|
||||
#define MQTT_MAX_HOSTNAME_STRLEN 128
|
||||
#define MQTT_MAX_CLIENTID_STRLEN 64
|
||||
#define MQTT_MAX_USERNAME_STRLEN 64
|
||||
#define MQTT_MAX_PASSWORD_STRLEN 64
|
||||
#define MQTT_MAX_TOPIC_STRLEN 32
|
||||
@ -88,6 +89,7 @@ struct CONFIG_T {
|
||||
bool Enabled;
|
||||
char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1];
|
||||
uint32_t Port;
|
||||
char ClientId[MQTT_MAX_CLIENTID_STRLEN + 1];
|
||||
char Username[MQTT_MAX_USERNAME_STRLEN + 1];
|
||||
char Password[MQTT_MAX_PASSWORD_STRLEN + 1];
|
||||
char Topic[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
|
||||
@ -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);
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -20,6 +20,7 @@ public:
|
||||
void unsubscribe(const String& topic);
|
||||
|
||||
String getPrefix() const;
|
||||
String getClientId();
|
||||
|
||||
private:
|
||||
void NetworkEvent(network_event event);
|
||||
@ -39,4 +40,4 @@ private:
|
||||
std::mutex _clientLock;
|
||||
};
|
||||
|
||||
extern MqttSettingsClass MqttSettings;
|
||||
extern MqttSettingsClass MqttSettings;
|
||||
|
||||
@ -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;
|
||||
extern NetworkSettingsClass NetworkSettings;
|
||||
|
||||
@ -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,10 +61,13 @@ public:
|
||||
|
||||
bool isValidNrf24Config() const;
|
||||
bool isValidCmt2300Config() const;
|
||||
bool isValidW5500Config() const;
|
||||
#if CONFIG_ETH_USE_ESP32_EMAC
|
||||
bool isValidEthConfig() const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
PinMapping_t _pinMapping;
|
||||
};
|
||||
|
||||
extern PinMappingClass PinMapping;
|
||||
extern PinMappingClass PinMapping;
|
||||
|
||||
18
include/RestartHelper.h
Normal file
18
include/RestartHelper.h
Normal 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;
|
||||
@ -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
29
include/W5500.h
Normal 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;
|
||||
};
|
||||
@ -30,6 +30,7 @@ class WebApiClass {
|
||||
public:
|
||||
WebApiClass();
|
||||
void init(Scheduler& scheduler);
|
||||
void reload();
|
||||
|
||||
static bool checkCredentials(AsyncWebServerRequest* request);
|
||||
static bool checkCredentialsReadonly(AsyncWebServerRequest* request);
|
||||
|
||||
@ -32,6 +32,7 @@ enum WebApiError {
|
||||
InverterChanged,
|
||||
InverterDeleted,
|
||||
InverterOrdered,
|
||||
InverterStatsResetted,
|
||||
|
||||
LimitBase = 5000,
|
||||
LimitSerialZero,
|
||||
@ -60,6 +61,7 @@ enum WebApiError {
|
||||
MqttHassTopicLength,
|
||||
MqttHassTopicCharacter,
|
||||
MqttLwtQos,
|
||||
MqttClientIdLength,
|
||||
|
||||
NetworkBase = 8000,
|
||||
NetworkIpInvalid,
|
||||
|
||||
@ -14,4 +14,5 @@ private:
|
||||
void onInverterEdit(AsyncWebServerRequest* request);
|
||||
void onInverterDelete(AsyncWebServerRequest* request);
|
||||
void onInverterOrder(AsyncWebServerRequest* request);
|
||||
void onInverterStatReset(AsyncWebServerRequest* request);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 };
|
||||
|
||||
|
||||
@ -5,4 +5,5 @@
|
||||
|
||||
|
||||
extern const char *__COMPILED_GIT_HASH__;
|
||||
extern const char *__COMPILED_GIT_BRANCH__;
|
||||
// extern const char *__COMPILED_DATE_TIME_UTC_STR__;
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
#define ACCESS_POINT_NAME "OpenDTU-"
|
||||
#define ACCESS_POINT_PASSWORD "openDTU42"
|
||||
#define ACCESS_POINT_TIMEOUT 3;
|
||||
#define ACCESS_POINT_TIMEOUT 3
|
||||
#define AUTH_USERNAME "admin"
|
||||
#define SECURITY_ALLOW_READONLY true
|
||||
|
||||
|
||||
@ -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
155
lib/CMT2300a/cmt_spi3.cpp
Normal 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();
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -85,7 +85,7 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons
|
||||
|
||||
float ActivePowerControlCommand::getLimit() const
|
||||
{
|
||||
const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]);
|
||||
const float l = (((uint16_t)_payload[12] << 8) | _payload[13]);
|
||||
return l / 10;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
55
lib/Hoymiles/src/inverters/HERF_1CH.cpp
Normal file
55
lib/Hoymiles/src/inverters/HERF_1CH.cpp
Normal 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]);
|
||||
}
|
||||
13
lib/Hoymiles/src/inverters/HERF_1CH.h
Normal file
13
lib/Hoymiles/src/inverters/HERF_1CH.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
return preSerial == 0x1144 || preSerial == 0x1143 || preSerial == 0x1410;
|
||||
}
|
||||
|
||||
String HMS_2CH::typeName() const
|
||||
|
||||
@ -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 = {};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 | 1144 |
|
||||
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
|
||||
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
|
||||
| HMT_6CH | HMT-1800/2250-6T | 1382 |
|
||||
| HERF_2CH | HERF 800 | 2821 |
|
||||
| HERF_4CH | HERF 1800 | 2801 |
|
||||
| Class | Models | Serial range |
|
||||
| --------------| --------------------------- | ------------- -- |
|
||||
| HM_1CH | HM-300/350/400-1T | 1121 |
|
||||
| HM_2CH | HM-600/700/800-2T | 1141 |
|
||||
| HM_4CH | HM-1000/1200/1500-4T | 1161 |
|
||||
| HMS_1CH | HMS-300/350/400/450/500-1T | 1124 |
|
||||
| HMS_1CHv2 | HMS-500-1T v2 | 1125 |
|
||||
| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 |
|
||||
| HMS_4CH | HMS-1600/1800/2000-4T | 1164 |
|
||||
| HMT_4CH | HMT-1600/1800/2000-4T | 1361 |
|
||||
| HMT_6CH | HMT-1800/2250-6T | 1382 |
|
||||
| HERF_1CH | HERF 300 | 2841 |
|
||||
| HERF_2CH | HERF 800 | 2821 |
|
||||
| HERF_4CH | HERF 1800 | 2801 |
|
||||
|
||||
@ -28,15 +28,15 @@ ID Source Addr Target Addr Idx ? wcode ? Start End ?
|
||||
|
||||
const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> AlarmLogParser::_alarmMessages = { {
|
||||
{ AlarmMessageType_t::ALL, 1, "Inverter start", "Wechselrichter gestartet", "L'onduleur a démarré" },
|
||||
{ AlarmMessageType_t::ALL, 2, "Time calibration", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 2, "Time calibration", "Zeitabgleich", "" },
|
||||
{ AlarmMessageType_t::ALL, 3, "EEPROM reading and writing error during operation", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 4, "Offline", "Offline", "Non connecté" },
|
||||
|
||||
{ AlarmMessageType_t::ALL, 11, "Grid voltage surge", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 14, "Grid phase mutation", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "", "" },
|
||||
{ AlarmMessageType_t::ALL, 11, "Grid voltage surge", "Netz: Überspannungsimpuls", "" },
|
||||
{ AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "Netz: Spannungseinbruch", "" },
|
||||
{ AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "Netz: Frequenzänderung", "" },
|
||||
{ AlarmMessageType_t::ALL, 14, "Grid phase mutation", "Netz: Phasenänderung", "" },
|
||||
{ AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "Netz: vorübergehende Schwankung", "" },
|
||||
|
||||
{ AlarmMessageType_t::ALL, 36, "INV overvoltage or overcurrent", "", "" },
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ const devInfo_t devInfo[] = {
|
||||
{ { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02
|
||||
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01
|
||||
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00
|
||||
{ { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00
|
||||
{ { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01
|
||||
{ { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900-2T" }, // 03
|
||||
{ { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 05
|
||||
|
||||
13
lib/SpiManager/library.json
Normal file
13
lib/SpiManager/library.json
Normal 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"
|
||||
]
|
||||
}
|
||||
52
lib/SpiManager/src/SpiBus.cpp
Normal file
52
lib/SpiManager/src/SpiBus.cpp
Normal 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);
|
||||
}
|
||||
49
lib/SpiManager/src/SpiBus.h
Normal file
49
lib/SpiManager/src/SpiBus.h
Normal 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;
|
||||
};
|
||||
71
lib/SpiManager/src/SpiBusConfig.cpp
Normal file
71
lib/SpiManager/src/SpiBusConfig.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
21
lib/SpiManager/src/SpiBusConfig.h
Normal file
21
lib/SpiManager/src/SpiBusConfig.h
Normal 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;
|
||||
};
|
||||
69
lib/SpiManager/src/SpiCallback.cpp
Normal file
69
lib/SpiManager/src/SpiCallback.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
15
lib/SpiManager/src/SpiCallback.h
Normal file
15
lib/SpiManager/src/SpiCallback.h
Normal 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);
|
||||
}
|
||||
114
lib/SpiManager/src/SpiManager.cpp
Normal file
114
lib/SpiManager/src/SpiManager.cpp
Normal 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;
|
||||
41
lib/SpiManager/src/SpiManager.h
Normal file
41
lib/SpiManager/src/SpiManager.h
Normal 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;
|
||||
@ -3,33 +3,27 @@
|
||||
# Copyright (C) 2022 Thomas Basler and others
|
||||
#
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
Import("env")
|
||||
|
||||
required_pkgs = {'dulwich'}
|
||||
installed_pkgs = {pkg.key for pkg in pkg_resources.working_set}
|
||||
missing_pkgs = required_pkgs - installed_pkgs
|
||||
|
||||
if missing_pkgs:
|
||||
try:
|
||||
from dulwich import porcelain
|
||||
except ModuleNotFoundError:
|
||||
env.Execute('"$PYTHONEXE" -m pip install dulwich')
|
||||
|
||||
from dulwich import porcelain
|
||||
from dulwich import porcelain
|
||||
|
||||
|
||||
def updateFileIfChanged(filename, content):
|
||||
mustUpdate = True
|
||||
try:
|
||||
fp = open(filename, "rb")
|
||||
if fp.read() == content:
|
||||
mustUpdate = False
|
||||
fp.close()
|
||||
with open(filename, "rb") as fp:
|
||||
if fp.read() == content:
|
||||
mustUpdate = False
|
||||
except:
|
||||
pass
|
||||
if mustUpdate:
|
||||
fp = open(filename, "wb")
|
||||
fp.write(content)
|
||||
fp.close()
|
||||
with open(filename, "wb") as fp:
|
||||
fp.write(content)
|
||||
return mustUpdate
|
||||
|
||||
|
||||
@ -42,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)
|
||||
|
||||
|
||||
@ -70,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"))
|
||||
|
||||
|
||||
@ -18,20 +18,64 @@
|
||||
|
||||
Import("env")
|
||||
|
||||
env = DefaultEnvironment()
|
||||
platform = env.PioPlatform()
|
||||
|
||||
import sys
|
||||
from os.path import join, getsize
|
||||
import csv
|
||||
import subprocess
|
||||
import shutil
|
||||
from os.path import join, getsize, exists, isdir
|
||||
from os import listdir
|
||||
|
||||
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
|
||||
import esptool
|
||||
|
||||
def esp32_build_filesystem(fs_name, fs_size):
|
||||
filesystem_dir = env.subst("$PROJECT_DATA_DIR")
|
||||
print("Creating %dKiB filesystem with content:" % (int(fs_size, 0)/1024) )
|
||||
if not isdir(filesystem_dir) or not listdir(filesystem_dir):
|
||||
print("No files added -> will NOT create littlefs.bin and NOT overwrite fs partition!")
|
||||
return False
|
||||
# this does not work on GitHub, results in 'mklittlefs: No such file or directory'
|
||||
tool = shutil.which(env.subst(env["MKFSTOOL"]))
|
||||
if tool is None or not exists(tool):
|
||||
print("Using fallback mklittlefs")
|
||||
tool = "~/.platformio/packages/tool-mklittlefs/mklittlefs"
|
||||
|
||||
cmd = (tool, "-c", filesystem_dir, "-s", fs_size, fs_name)
|
||||
returncode = subprocess.call(cmd, shell=False)
|
||||
print("Return Code:", returncode)
|
||||
return True
|
||||
|
||||
def esp32_create_combined_bin(source, target, env):
|
||||
print("Generating combined binary for serial flashing")
|
||||
|
||||
# The offset from begin of the file where the app0 partition starts
|
||||
# This is defined in the partition .csv file
|
||||
app_offset = 0x10000
|
||||
fs_offset = -1
|
||||
fs_name = env.subst("$BUILD_DIR/littlefs.bin")
|
||||
|
||||
with open(env.BoardConfig().get("build.partitions")) as csv_file:
|
||||
print("Read partitions from ", env.BoardConfig().get("build.partitions"))
|
||||
csv_reader = csv.reader(csv_file, delimiter=',')
|
||||
line_count = 0
|
||||
for row in csv_reader:
|
||||
if line_count == 0:
|
||||
print(f'{", ".join(row)}')
|
||||
line_count += 1
|
||||
else:
|
||||
if (len(row) < 4):
|
||||
continue
|
||||
print(f'{row[0]} {row[1]} {row[2]} {row[3]} {row[4]}')
|
||||
line_count += 1
|
||||
if(row[0] == 'app0'):
|
||||
app_offset = int(row[3], base=16)
|
||||
elif(row[0] == 'spiffs'):
|
||||
partition_size = row[4]
|
||||
if esp32_build_filesystem(fs_name, partition_size):
|
||||
fs_offset = int(row[3], base=16)
|
||||
|
||||
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
|
||||
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
|
||||
@ -77,9 +121,13 @@ def esp32_create_combined_bin(source, target, env):
|
||||
print(f" - {hex(app_offset)} | {firmware_name}")
|
||||
cmd += [hex(app_offset), firmware_name]
|
||||
|
||||
if fs_offset != -1:
|
||||
print(f" - {hex(fs_offset)} | {fs_name}")
|
||||
cmd += [hex(fs_offset), fs_name]
|
||||
|
||||
print('Using esptool.py arguments: %s' % ' '.join(cmd))
|
||||
|
||||
esptool.main(cmd)
|
||||
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)
|
||||
|
||||
@ -19,7 +19,9 @@ extra_configs =
|
||||
custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb
|
||||
|
||||
framework = arduino
|
||||
platform = espressif32@6.7.0
|
||||
platform = espressif32@6.9.0
|
||||
platform_packages =
|
||||
platformio/tool-mklittlefs
|
||||
|
||||
build_flags =
|
||||
-DPIOENV=\"$PIOENV\"
|
||||
@ -27,6 +29,7 @@ build_flags =
|
||||
-D_TASK_THREAD_SAFE=1
|
||||
-DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128
|
||||
-DCONFIG_ASYNC_TCP_QUEUE_SIZE=128
|
||||
-DEMC_TASK_STACK_SIZE=6400
|
||||
-Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference
|
||||
; Have to remove -Werror because of
|
||||
; https://github.com/espressif/arduino-esp32/issues/9044 and
|
||||
@ -38,13 +41,13 @@ build_unflags =
|
||||
-std=gnu++11
|
||||
|
||||
lib_deps =
|
||||
mathieucarbou/ESP Async WebServer @ 2.10.8
|
||||
bblanchon/ArduinoJson @ 7.0.4
|
||||
mathieucarbou/ESPAsyncWebServer @ 3.3.12
|
||||
bblanchon/ArduinoJson @ 7.2.0
|
||||
https://github.com/bertmelis/espMqttClient.git#v1.7.0
|
||||
nrf24/RF24 @ 1.4.8
|
||||
olikraus/U8g2 @ 2.35.19
|
||||
nrf24/RF24 @ 1.4.9
|
||||
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
|
||||
@ -226,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
|
||||
@ -249,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
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "Utils.h"
|
||||
#include "defaults.h"
|
||||
#include <ArduinoJson.h>
|
||||
@ -58,6 +59,7 @@ bool ConfigurationClass::write()
|
||||
mqtt["enabled"] = config.Mqtt.Enabled;
|
||||
mqtt["hostname"] = config.Mqtt.Hostname;
|
||||
mqtt["port"] = config.Mqtt.Port;
|
||||
mqtt["clientid"] = config.Mqtt.ClientId;
|
||||
mqtt["username"] = config.Mqtt.Username;
|
||||
mqtt["password"] = config.Mqtt.Password;
|
||||
mqtt["topic"] = config.Mqtt.Topic;
|
||||
@ -232,6 +234,7 @@ bool ConfigurationClass::read()
|
||||
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
|
||||
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
|
||||
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
|
||||
strlcpy(config.Mqtt.ClientId, mqtt["clientid"] | NetworkSettings.getApName().c_str(), sizeof(config.Mqtt.ClientId));
|
||||
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
|
||||
strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password));
|
||||
strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100);
|
||||
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)
|
||||
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,149 +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)
|
||||
@ -432,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);
|
||||
}
|
||||
|
||||
@ -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,91 +173,98 @@ 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 int32_t payload_val = strtol(strlimit, NULL, 10);
|
||||
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: %d %%\r\n", payload_val);
|
||||
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: %d W\r\n", payload_val);
|
||||
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: %d %%\r\n", payload_val);
|
||||
MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val);
|
||||
if (!properties.retain) {
|
||||
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativNonPersistent);
|
||||
} 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: %d W\r\n", payload_val);
|
||||
MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val);
|
||||
if (!properties.retain) {
|
||||
inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutNonPersistent);
|
||||
} 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", payload_val);
|
||||
inv->sendPowerControlRequest(payload_val > 0);
|
||||
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) {
|
||||
inv->sendRestartControlRequest();
|
||||
} else {
|
||||
MessageOutput.println("Ignored because retained");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ void MqttSettingsClass::performConnect()
|
||||
MessageOutput.println("Connecting to MQTT...");
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic;
|
||||
const String clientId = NetworkSettings.getApName();
|
||||
String clientId = getClientId();
|
||||
if (config.Mqtt.Tls.Enabled) {
|
||||
static_cast<espMqttClientSecure*>(_mqttClient)->setCACert(config.Mqtt.Tls.RootCaCert);
|
||||
static_cast<espMqttClientSecure*>(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port);
|
||||
@ -180,6 +180,15 @@ String MqttSettingsClass::getPrefix() const
|
||||
return Configuration.get().Mqtt.Topic;
|
||||
}
|
||||
|
||||
String MqttSettingsClass::getClientId()
|
||||
{
|
||||
String clientId = Configuration.get().Mqtt.ClientId;
|
||||
if (clientId == "") {
|
||||
clientId = NetworkSettings.getApName();
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
void MqttSettingsClass::publish(const String& subtopic, const String& payload)
|
||||
{
|
||||
String topic = getPrefix();
|
||||
|
||||
@ -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,18 +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.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1));
|
||||
WiFi.disconnect(true, true);
|
||||
|
||||
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:
|
||||
@ -74,10 +97,12 @@ 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.reconnect();
|
||||
WiFi.disconnect(true, false);
|
||||
WiFi.begin();
|
||||
raiseEvent(network_event::NETWORK_DISCONNECTED);
|
||||
}
|
||||
break;
|
||||
@ -92,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);
|
||||
@ -106,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);
|
||||
@ -164,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()
|
||||
@ -207,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++;
|
||||
@ -396,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:
|
||||
|
||||
@ -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
36
src/RestartHelper.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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
153
src/W5500.cpp
Normal 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(ð_config, ð_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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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__);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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__);
|
||||
}
|
||||
|
||||
@ -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__);
|
||||
@ -83,7 +83,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["limit_value"].as<uint16_t>() > MAX_INVERTER_LIMIT) {
|
||||
if (root["limit_value"].as<float>() > MAX_INVERTER_LIMIT) {
|
||||
retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = MAX_INVERTER_LIMIT;
|
||||
@ -102,7 +102,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t limit = root["limit_value"].as<uint16_t>();
|
||||
float limit = root["limit_value"].as<float>();
|
||||
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -34,6 +34,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
root["mqtt_enabled"] = config.Mqtt.Enabled;
|
||||
root["mqtt_hostname"] = config.Mqtt.Hostname;
|
||||
root["mqtt_port"] = config.Mqtt.Port;
|
||||
root["mqtt_clientid"] = MqttSettings.getClientId();
|
||||
root["mqtt_username"] = config.Mqtt.Username;
|
||||
root["mqtt_topic"] = config.Mqtt.Topic;
|
||||
root["mqtt_connected"] = MqttSettings.getConnected();
|
||||
@ -67,6 +68,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
root["mqtt_enabled"] = config.Mqtt.Enabled;
|
||||
root["mqtt_hostname"] = config.Mqtt.Hostname;
|
||||
root["mqtt_port"] = config.Mqtt.Port;
|
||||
root["mqtt_clientid"] = config.Mqtt.ClientId;
|
||||
root["mqtt_username"] = config.Mqtt.Username;
|
||||
root["mqtt_password"] = config.Mqtt.Password;
|
||||
root["mqtt_topic"] = config.Mqtt.Topic;
|
||||
@ -105,28 +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_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__);
|
||||
@ -142,6 +145,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (root["mqtt_clientid"].as<String>().length() > MQTT_MAX_CLIENTID_STRLEN) {
|
||||
retMsg["message"] = "Client ID must not be longer than " STR(MQTT_MAX_CLIENTID_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttClientIdLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_CLIENTID_STRLEN;
|
||||
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
|
||||
return;
|
||||
}
|
||||
if (root["mqtt_username"].as<String>().length() > MQTT_MAX_USERNAME_STRLEN) {
|
||||
retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttUsernameLength;
|
||||
@ -271,6 +281,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
strlcpy(config.Mqtt.Tls.ClientKey, root["mqtt_client_key"].as<String>().c_str(), sizeof(config.Mqtt.Tls.ClientKey));
|
||||
config.Mqtt.Port = root["mqtt_port"].as<uint>();
|
||||
strlcpy(config.Mqtt.Hostname, root["mqtt_hostname"].as<String>().c_str(), sizeof(config.Mqtt.Hostname));
|
||||
strlcpy(config.Mqtt.ClientId, root["mqtt_clientid"].as<String>().c_str(), sizeof(config.Mqtt.ClientId));
|
||||
strlcpy(config.Mqtt.Username, root["mqtt_username"].as<String>().c_str(), sizeof(config.Mqtt.Username));
|
||||
strlcpy(config.Mqtt.Password, root["mqtt_password"].as<String>().c_str(), sizeof(config.Mqtt.Password));
|
||||
strlcpy(config.Mqtt.Lwt.Topic, root["mqtt_lwt_topic"].as<String>().c_str(), sizeof(config.Mqtt.Lwt.Topic));
|
||||
|
||||
@ -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__);
|
||||
|
||||
@ -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__);
|
||||
|
||||
@ -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>()) {
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -42,7 +42,7 @@ void WebApiWebappClass::responseBinaryDataWithETagCache(AsyncWebServerRequest *r
|
||||
if (eTagMatch) {
|
||||
response = request->beginResponse(304);
|
||||
} else {
|
||||
response = request->beginResponse_P(200, contentType, content, len);
|
||||
response = request->beginResponse(200, contentType, content, len);
|
||||
if (contentEncoding.length() > 0) {
|
||||
response->addHeader("Content-Encoding", contentEncoding);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
13
src/main.cpp
13
src/main.cpp
@ -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()
|
||||
|
||||
8
webapp/.prettierrc.json
Normal file
8
webapp/.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
# OpenDTU web frontend
|
||||
|
||||
You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.config.ts` beforehand, all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser.
|
||||
You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.user.ts` beforehand (template can be found in `vite.config.ts`), all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser.
|
||||
|
||||
## Project Setup
|
||||
|
||||
@ -24,4 +24,4 @@ yarn build
|
||||
|
||||
```sh
|
||||
yarn lint
|
||||
```
|
||||
```
|
||||
|
||||
8
webapp/env.d.ts
vendored
8
webapp/env.d.ts
vendored
@ -1 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { Router, Route } from 'vue-router'
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$router: Router
|
||||
$route: Route
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
@ -30,7 +20,7 @@ export default [
|
||||
"**/*.mts",
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest'
|
||||
ecmaVersion: 2022
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
@ -9,40 +9,43 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"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.27",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.3"
|
||||
"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": "^20.14.2",
|
||||
"@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.0.5",
|
||||
"@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.4.0",
|
||||
"eslint-plugin-vue": "^9.26.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.4",
|
||||
"terser": "^5.31.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.13",
|
||||
"sass": "^1.77.6",
|
||||
"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.21"
|
||||
}
|
||||
"vite-plugin-css-injected-by-js": "^3.5.2",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
import NavBar from './components/NavBar.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
name: 'App',
|
||||
components: {
|
||||
NavBar,
|
||||
},
|
||||
|
||||
@ -1,19 +1,29 @@
|
||||
<template>
|
||||
<div :class="{'container-xxl': !isWideScreen,
|
||||
'container-fluid': isWideScreen}" role="main">
|
||||
<div :class="{ 'container-xxl': !isWideScreen, 'container-fluid': isWideScreen }" role="main">
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-sm-11">
|
||||
<h1>{{ title }}
|
||||
<span v-if="showWebSocket" :class="{
|
||||
'onlineMarker': isWebsocketConnected,
|
||||
'offlineMarker': !isWebsocketConnected,
|
||||
}"></span>
|
||||
<h1>
|
||||
{{ title }}
|
||||
<span
|
||||
v-if="showWebSocket"
|
||||
:class="{
|
||||
onlineMarker: isWebsocketConnected,
|
||||
offlineMarker: !isWebsocketConnected,
|
||||
}"
|
||||
></span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="col-sm-1" v-if="showReload">
|
||||
<button type="button" class="float-end btn btn-outline-primary"
|
||||
@click="$emit('reload')" v-tooltip :title="$t('base.Reload')" ><BIconArrowClockwise /></button>
|
||||
<button
|
||||
type="button"
|
||||
class="float-end btn btn-outline-primary"
|
||||
@click="$emit('reload')"
|
||||
v-tooltip
|
||||
:title="$t('base.Reload')"
|
||||
>
|
||||
<BIconArrowClockwise />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -48,7 +58,7 @@ export default defineComponent({
|
||||
showReload: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
mounted() {
|
||||
console.log("init");
|
||||
console.log('init');
|
||||
PullToRefresh.init({
|
||||
mainElement: 'body', // above which element?
|
||||
instructionsPullToRefresh: this.$t('base.Pull'),
|
||||
@ -56,11 +66,11 @@ export default defineComponent({
|
||||
instructionsRefreshing: this.$t('base.Refreshing'),
|
||||
onRefresh: () => {
|
||||
this.$emit('reload');
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
unmounted() {
|
||||
console.log("destroy");
|
||||
console.log('destroy');
|
||||
PullToRefresh.destroyAll();
|
||||
},
|
||||
});
|
||||
@ -100,13 +110,15 @@ export default defineComponent({
|
||||
margin: -12px 0 0 -12px;
|
||||
border: 1px solid #00bb00;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 4px #00bb00, inset 0 0 4px rgb(56, 111, 169);
|
||||
box-shadow:
|
||||
0 0 4px #00bb00,
|
||||
inset 0 0 4px rgb(56, 111, 169);
|
||||
transform: scale(0);
|
||||
animation: online 2.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes online {
|
||||
0% {
|
||||
transform: scale(.1);
|
||||
transform: scale(0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -1,44 +1,50 @@
|
||||
<template>
|
||||
<div v-if="isAlertVisible" ref="element" class="alert" role="alert" :class="classes">
|
||||
<slot />
|
||||
<button v-if="dismissible" type="button" class="btn-close" data-bs-dismiss="alert" :aria-label="dismissLabel"
|
||||
@click="dismissClicked" />
|
||||
<button
|
||||
v-if="dismissible"
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="alert"
|
||||
:aria-label="dismissLabel"
|
||||
@click="dismissClicked"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Alert from "bootstrap/js/dist/alert";
|
||||
import { computed, defineComponent, onBeforeUnmount, ref, watch } from "vue";
|
||||
import Alert from 'bootstrap/js/dist/alert';
|
||||
import { computed, defineComponent, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
export const toInteger = (value: number, defaultValue = NaN) => {
|
||||
return Number.isInteger(value) ? value : defaultValue;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "BootstrapAlert",
|
||||
name: 'BootstrapAlert',
|
||||
props: {
|
||||
dismissLabel: { type: String, default: "Close" },
|
||||
dismissLabel: { type: String, default: 'Close' },
|
||||
dismissible: { type: Boolean, default: false },
|
||||
fade: { type: Boolean, default: false },
|
||||
modelValue: { type: [Boolean, Number], default: false },
|
||||
show: { type: Boolean, default: false },
|
||||
variant: { type: String, default: "info" },
|
||||
variant: { type: String, default: 'info' },
|
||||
},
|
||||
emits: ["dismissed", "dismiss-count-down", "update:modelValue"],
|
||||
emits: ['dismissed', 'dismiss-count-down', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const element = ref<HTMLElement>();
|
||||
const instance = ref<Alert>();
|
||||
const classes = computed(() => ({
|
||||
[`alert-${props.variant}`]: props.variant,
|
||||
show: props.modelValue,
|
||||
"alert-dismissible": props.dismissible,
|
||||
'alert-dismissible': props.dismissible,
|
||||
fade: props.modelValue,
|
||||
}));
|
||||
|
||||
let _countDownTimeout: number | undefined = 0;
|
||||
|
||||
const parseCountDown = (value: boolean | number) => {
|
||||
if (typeof value === "boolean") {
|
||||
if (typeof value === 'boolean') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -53,9 +59,12 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const countDown = ref();
|
||||
watch(() => props.modelValue, () => {
|
||||
countDown.value = parseCountDown(props.modelValue);
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
countDown.value = parseCountDown(props.modelValue);
|
||||
}
|
||||
);
|
||||
|
||||
const isAlertVisible = computed(() => props.modelValue || props.show);
|
||||
|
||||
@ -85,12 +94,12 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const dismissClicked = () => {
|
||||
if (typeof props.modelValue === "boolean") {
|
||||
emit("update:modelValue", false);
|
||||
if (typeof props.modelValue === 'boolean') {
|
||||
emit('update:modelValue', false);
|
||||
} else {
|
||||
emit("update:modelValue", 0);
|
||||
emit('update:modelValue', 0);
|
||||
}
|
||||
emit("dismissed");
|
||||
emit('dismissed');
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, handleShowAndModelChanged);
|
||||
@ -98,10 +107,10 @@ export default defineComponent({
|
||||
|
||||
watch(countDown, (newValue) => {
|
||||
clearCountDownInterval();
|
||||
if (typeof props.modelValue === "boolean") return;
|
||||
emit("dismiss-count-down", newValue);
|
||||
if (newValue === 0 && props.modelValue > 0) emit("dismissed");
|
||||
if (props.modelValue !== newValue) emit("update:modelValue", newValue);
|
||||
if (typeof props.modelValue === 'boolean') return;
|
||||
emit('dismiss-count-down', newValue);
|
||||
if (newValue === 0 && props.modelValue > 0) emit('dismissed');
|
||||
if (props.modelValue !== newValue) emit('update:modelValue', newValue);
|
||||
if (newValue > 0) {
|
||||
_countDownTimeout = setTimeout(() => {
|
||||
countDown.value--;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="['card', addSpace ? 'mt-5' : '' ]">
|
||||
<div :class="['card', addSpace ? 'mt-5' : '']">
|
||||
<div :class="['card-header', textVariant]">{{ text }}</div>
|
||||
<div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']">
|
||||
<slot />
|
||||
@ -12,10 +12,10 @@ import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
'text': String,
|
||||
'textVariant': String,
|
||||
'addSpace': Boolean,
|
||||
'centerContent': Boolean,
|
||||
text: String,
|
||||
textVariant: String,
|
||||
addSpace: Boolean,
|
||||
centerContent: Boolean,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<BootstrapAlert :show="!devInfoList.valid_data">
|
||||
<h4 class="alert-heading">
|
||||
<BIconInfoSquare class="fs-2" /> {{ $t('devinfo.NoInfo') }}
|
||||
</h4>{{ $t('devinfo.NoInfoLong') }}
|
||||
<h4 class="alert-heading"><BIconInfoSquare class="fs-2" /> {{ $t('devinfo.NoInfo') }}</h4>
|
||||
{{ $t('devinfo.NoInfoLong') }}
|
||||
</BootstrapAlert>
|
||||
<table v-if="devInfoList.valid_data" class="table table-hover">
|
||||
<tbody>
|
||||
@ -53,7 +52,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import type { DevInfoStatus } from "@/types/DevInfoStatus";
|
||||
import type { DevInfoStatus } from '@/types/DevInfoStatus';
|
||||
import { BIconInfoSquare } from 'bootstrap-icons-vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
@ -70,20 +69,20 @@ export default defineComponent({
|
||||
return (value: number) => {
|
||||
const version_major = Math.floor(value / 10000);
|
||||
const version_minor = Math.floor((value - version_major * 10000) / 100);
|
||||
const version_patch = Math.floor((value - version_major * 10000 - version_minor * 100));
|
||||
return version_major + "." + version_minor + "." + version_patch;
|
||||
const version_patch = Math.floor(value - version_major * 10000 - version_minor * 100);
|
||||
return version_major + '.' + version_minor + '.' + version_patch;
|
||||
};
|
||||
},
|
||||
productionYear() {
|
||||
return() => {
|
||||
return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014;
|
||||
}
|
||||
return () => {
|
||||
return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xf) + 2014;
|
||||
};
|
||||
},
|
||||
productionWeek() {
|
||||
return() => {
|
||||
return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xFF).toString(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xff).toString(16);
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th scope="col">{{ $t('eventlog.Start') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Stop') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Id') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Message') }}</th>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('eventlog.Start') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Stop') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Id') }}</th>
|
||||
<th scope="col">{{ $t('eventlog.Message') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="event in eventLogList.count" :key="event">
|
||||
@ -36,4 +38,4 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -17,10 +17,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('firmwareinfo.FirmwareVersion') }}</th>
|
||||
<td><a :href="versionInfoUrl"
|
||||
target="_blank" v-tooltip :title="$t('firmwareinfo.FirmwareVersionHint')">
|
||||
<td>
|
||||
<a
|
||||
:href="versionInfoUrl"
|
||||
target="_blank"
|
||||
v-tooltip
|
||||
:title="$t('firmwareinfo.FirmwareVersionHint')"
|
||||
>
|
||||
{{ systemStatus.git_hash }}
|
||||
</a></td>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('firmwareinfo.PioEnv') }}</th>
|
||||
@ -30,16 +36,32 @@
|
||||
<th>{{ $t('firmwareinfo.FirmwareUpdate') }}</th>
|
||||
<td>
|
||||
<div class="form-check form-check-inline form-switch">
|
||||
<input v-model="modelAllowVersionInfo" class="form-check-input" type="checkbox" role="switch" v-tooltip :title="$t('firmwareinfo.FrmwareUpdateAllow')" />
|
||||
<input
|
||||
v-model="modelAllowVersionInfo"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
v-tooltip
|
||||
:title="$t('firmwareinfo.FrmwareUpdateAllow')"
|
||||
/>
|
||||
<label class="form-check-label">
|
||||
<a v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined" :href="systemStatus.update_url" target="_blank" v-tooltip
|
||||
:title="$t('firmwareinfo.FirmwareUpdateHint')">
|
||||
<a
|
||||
v-if="modelAllowVersionInfo && systemStatus.update_url !== undefined"
|
||||
:href="systemStatus.update_url"
|
||||
target="_blank"
|
||||
v-tooltip
|
||||
:title="$t('firmwareinfo.FirmwareUpdateHint')"
|
||||
>
|
||||
<span class="badge" :class="systemStatus.update_status">
|
||||
{{ systemStatus.update_text }}
|
||||
</span>
|
||||
</a>
|
||||
<span v-else-if="modelAllowVersionInfo" class="badge" :class="systemStatus.update_status">
|
||||
{{ systemStatus.update_text }}
|
||||
<span
|
||||
v-else-if="modelAllowVersionInfo"
|
||||
class="badge"
|
||||
:class="systemStatus.update_status"
|
||||
>
|
||||
{{ systemStatus.update_text }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -59,7 +81,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('firmwareinfo.Uptime') }}</th>
|
||||
<td>{{ $t('firmwareinfo.UptimeValue', timeInHours(systemStatus.uptime)) }}</td>
|
||||
<td>
|
||||
{{ $t('firmwareinfo.UptimeValue', timeInHours(systemStatus.uptime)) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -93,7 +117,7 @@ export default defineComponent({
|
||||
timeInHours() {
|
||||
return (value: number) => {
|
||||
const [count, time] = timestampToString(this.$i18n.locale, value, true);
|
||||
return {count, time};
|
||||
return { count, time };
|
||||
};
|
||||
},
|
||||
versionInfoUrl(): string {
|
||||
@ -101,7 +125,7 @@ export default defineComponent({
|
||||
return 'https://github.com/tbnobody/OpenDTU/commits/' + this.systemStatus.git_hash;
|
||||
}
|
||||
return 'https://github.com/tbnobody/OpenDTU/releases/tag/' + this.systemStatus.git_hash;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<hr class="border border-3 opacity-75">
|
||||
<hr class="border border-3 opacity-75" />
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('reload')">{{ $t('base.Cancel') }}</button>
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('reload')">
|
||||
{{ $t('base.Cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">{{ $t('base.Save') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -3,8 +3,14 @@
|
||||
<th>{{ name }}</th>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" :style="{ width: getPercent() + '%' }"
|
||||
v-bind:aria-valuenow="getPercent()" aria-valuemin="0" aria-valuemax="100">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: getPercent() + '%' }"
|
||||
v-bind:aria-valuenow="getPercent()"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
{{ $n(getPercent() / 100, 'percent') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,47 +1,57 @@
|
||||
<template>
|
||||
<BootstrapAlert :show="!hasValidData">
|
||||
<h4 class="alert-heading">
|
||||
<BIconInfoSquare class="fs-2" /> {{ $t('gridprofile.NoInfo') }}
|
||||
</h4>{{ $t('gridprofile.NoInfoLong') }}
|
||||
<h4 class="alert-heading"><BIconInfoSquare class="fs-2" /> {{ $t('gridprofile.NoInfo') }}</h4>
|
||||
{{ $t('gridprofile.NoInfoLong') }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<template v-if="hasValidData">
|
||||
<table class="table table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('gridprofile.Name') }}</td>
|
||||
<td>{{ gridProfileList.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('gridprofile.Version') }}</td>
|
||||
<td>{{ gridProfileList.version }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('gridprofile.Name') }}</td>
|
||||
<td>{{ gridProfileList.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('gridprofile.Version') }}</td>
|
||||
<td>{{ gridProfileList.version }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="accordion" id="accordionProfile">
|
||||
<div class="accordion-item" v-for="(section, index) in gridProfileList.sections" :key="index">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" :data-bs-target="`#collapse${index}`" aria-expanded="true" :aria-controls="`collapse${index}`">
|
||||
<button
|
||||
class="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
:data-bs-target="`#collapse${index}`"
|
||||
aria-expanded="true"
|
||||
:aria-controls="`collapse${index}`"
|
||||
>
|
||||
{{ section.name }}
|
||||
</button>
|
||||
</h2>
|
||||
<div :id="`collapse${index}`" class="accordion-collapse collapse" data-bs-parent="#accordionProfile">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-hover">
|
||||
<tbody>
|
||||
<tr v-for="value in section.items" :key="value.n">
|
||||
<th>{{ value.n }}</th>
|
||||
<td>
|
||||
<template v-if="value.u!='bool'">
|
||||
{{ $n(value.v, 'decimal') }} {{ value.u }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<StatusBadge :status="value.v==1" true_text="gridprofile.Enabled" false_text="gridprofile.Disabled"/>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-for="value in section.items" :key="value.n">
|
||||
<th>{{ value.n }}</th>
|
||||
<td>
|
||||
<template v-if="value.u != 'bool'">
|
||||
{{ $n(value.v, 'decimal') }} {{ value.u }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<StatusBadge
|
||||
:status="value.v == 1"
|
||||
true_text="gridprofile.Enabled"
|
||||
false_text="gridprofile.Disabled"
|
||||
/>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -53,7 +63,14 @@
|
||||
<div class="accordion" id="accordionDev">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDev" aria-expanded="true" aria-controls="collapseDev">
|
||||
<button
|
||||
class="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseDev"
|
||||
aria-expanded="true"
|
||||
aria-controls="collapseDev"
|
||||
>
|
||||
{{ $t('gridprofile.GridprofileSupport') }}
|
||||
</button>
|
||||
</h2>
|
||||
@ -62,7 +79,8 @@
|
||||
<BootstrapAlert :show="true" variant="danger">
|
||||
<h4 class="info-heading">
|
||||
<BIconInfoSquare class="fs-2" /> {{ $t('gridprofile.GridprofileSupport') }}
|
||||
</h4><div v-html="$t('gridprofile.GridprofileSupportLong')"></div>
|
||||
</h4>
|
||||
<div v-html="$t('gridprofile.GridprofileSupportLong')"></div>
|
||||
</BootstrapAlert>
|
||||
<samp>
|
||||
{{ rawContent() }}
|
||||
@ -71,15 +89,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import type { GridProfileRawdata } from '@/types/GridProfileRawdata';
|
||||
import type { GridProfileStatus } from "@/types/GridProfileStatus";
|
||||
import type { GridProfileStatus } from '@/types/GridProfileStatus';
|
||||
import { BIconInfoSquare } from 'bootstrap-icons-vue';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
import StatusBadge from './StatusBadge.vue';
|
||||
@ -97,12 +113,14 @@ export default defineComponent({
|
||||
computed: {
|
||||
rawContent() {
|
||||
return () => {
|
||||
return this.gridProfileRawList.raw.map(function (x) {
|
||||
let y = x.toString(16); // to hex
|
||||
y = ("00" + y).substr(-2); // zero-pad to 2-digits
|
||||
return y
|
||||
}).join(' ');
|
||||
}
|
||||
return this.gridProfileRawList.raw
|
||||
.map(function (x) {
|
||||
let y = x.toString(16); // to hex
|
||||
y = ('00' + y).substr(-2); // zero-pad to 2-digits
|
||||
return y;
|
||||
})
|
||||
.join(' ');
|
||||
};
|
||||
},
|
||||
hasValidData() {
|
||||
return this.gridProfileRawList.raw.reduce((sum, x) => sum + x, 0) > 0;
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<th>{{ $t('hardwareinfo.CpuFrequency') }}</th>
|
||||
<td>{{ systemStatus.cpufreq }} {{ $t('hardwareinfo.Mhz') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr v-if="systemStatus.cputemp">
|
||||
<th>{{ $t('hardwareinfo.CpuTemperature') }}</th>
|
||||
<td>{{ $n(systemStatus.cputemp, 'celsius') }}</td>
|
||||
</tr>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user