Merge branch 'development'
This commit is contained in:
commit
724f1cf713
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
@ -107,31 +107,43 @@ jobs:
|
||||
.pio/build/${{ matrix.environment }}/bootloader.bin
|
||||
.pio/build/${{ matrix.environment }}/boot_app0.bin
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: opendtu-release
|
||||
path: |
|
||||
.pio/build/${{ matrix.environment }}/opendtu-${{ matrix.environment }}.bin
|
||||
.pio/build/${{ matrix.environment }}/partitions.bin
|
||||
.pio/build/${{ matrix.environment }}/bootloader_dio_40m.bin
|
||||
.pio/build/${{ matrix.environment }}/boot_app0.bin
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [get_default_envs, build]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: opendtu-release
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v1
|
||||
- name: Build Changelog
|
||||
id: github_release
|
||||
uses: mikepenz/release-changelog-builder-action@v3.7.0
|
||||
with:
|
||||
draft: True
|
||||
files: |
|
||||
*.bin
|
||||
failOnError: true
|
||||
commitMode: true
|
||||
configuration: ".github/workflows/config/release-notes-config.json"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts/
|
||||
|
||||
- name: Create ZIPs
|
||||
run: |
|
||||
ls -R
|
||||
sudo apt install zip
|
||||
cd artifacts
|
||||
for i in */; do zip -r "${i%/}.zip" "$i"; done
|
||||
for i in */; do cp ${i}opendtu-*.bin ./; done
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{steps.github_release.outputs.changelog}}
|
||||
draft: False
|
||||
files: |
|
||||
artifacts/*.zip, artifacts/*.bin
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
44
.github/workflows/config/release-notes-config.json
vendored
Normal file
44
.github/workflows/config/release-notes-config.json
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"title": "## ⚡ Breaking Changes",
|
||||
"labels": [
|
||||
"breaking change"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 🚀 Features",
|
||||
"labels": [
|
||||
"feature"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 🐛 Fixes",
|
||||
"labels": [
|
||||
"fix"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 📚 Documentation",
|
||||
"labels": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "## 🛠 Under the hood",
|
||||
"labels": []
|
||||
}
|
||||
],
|
||||
"template": "${{CHANGELOG}}",
|
||||
"pr_template": "- [${{TITLE}}](https://github.com/tbnobody/OpenDTU/commit/${{MERGE_SHA}})",
|
||||
"empty_template": "- no changes",
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "(.): (.+)",
|
||||
"target": "$1"
|
||||
}
|
||||
],
|
||||
"tag_resolver": {
|
||||
"method": "sort"
|
||||
}
|
||||
}
|
||||
2
.github/workflows/cpplint.yml
vendored
2
.github/workflows/cpplint.yml
vendored
@ -18,4 +18,4 @@ jobs:
|
||||
pip install cpplint
|
||||
- name: Linting
|
||||
run: |
|
||||
cpplint --repository=. --recursive --filter=-runtime/references,-readability/braces,-whitespace,-legal,-build/include ./src ./include ./lib/Hoymiles
|
||||
cpplint --repository=. --recursive --filter=-runtime/references,-readability/braces,-whitespace,-legal,-build/include ./src ./include ./lib/Hoymiles ./lib/MqttSubscribeParser ./lib/TimeoutHelper ./lib/ResetReason
|
||||
|
||||
22
.github/workflows/yarnlint.yml
vendored
Normal file
22
.github/workflows/yarnlint.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Yarn Linting
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js and yarn
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "webapp/yarn.lock"
|
||||
|
||||
- name: Install WebApp dependencies
|
||||
run: yarn --cwd webapp install --frozen-lockfile
|
||||
|
||||
- name: Linting
|
||||
run: yarn --cwd webapp lint
|
||||
22
README.md
22
README.md
@ -7,15 +7,17 @@ This is a fork from the Hoymiles project OpenDTU.
|
||||
This project is still under development and adds following features:
|
||||
|
||||
* Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded from https://www.victronenergy.com/support-and-downloads/technical-information.
|
||||
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household (needs an MQTT based power meter like Shelly 3EM)
|
||||
* Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter.
|
||||
* Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds
|
||||
* Voltage correction that takes the voltage drop because of the current output load into account (not 100% reliable calculation)
|
||||
* Can read the current solar panel power from the Victron MPPT and adjust the limiter accordingly to not save energy in the battery (for increased system efficiency). Increases the battery lifespan and reduces energy loses.
|
||||
* Settings can be configured in the UI
|
||||
* Pylontech Battery support (via CAN bus interface). Use the SOC for starting/stopping the power output and provide the battery data via MQTT (autodiscovery for home assistant is currently not supported). Pin Mapping is supported (default RX PIN 27, TX PIN 26). Actual no live view support for Pylontech Battery.
|
||||
* Huawei R4850G2 power supply unit that can act as AC charger. Supports status shown on the web interface and options to set voltage and current limits on the web interface and via MQTT. Connection is done using CAN bus (needs to be separate from Pylontech CAN bus) via SN65HVD230 interface.
|
||||
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
|
||||
[](https://github.com/tbnobody/OpenDTU/actions/workflows/yarnlint.yml)
|
||||
|
||||
## !! IMPORTANT UPGRADE NOTES !!
|
||||
|
||||
@ -166,6 +168,23 @@ Topics for 3 phases of a power meter is configurable. Given is an example for th
|
||||
| battery/charging/dischargeEnabled | R | | |
|
||||
| battery/charging/chargeImmediately | R | | |
|
||||
|
||||
## Huawei AC charger topics
|
||||
| Topic | R / W | Description | Value / Unit |
|
||||
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||
| huawei/cmd/limit_online_voltage | W | Online voltage (i.e. CAN bus connected) | Volt (V) |
|
||||
| huawei/cmd/limit_online_current | W | Online current (i.e. CAN bus connected) | Ampere (A) |
|
||||
| huawei/cmd/power | W | Controls output pin GPIO to drive solid state relais | 0 / 1 |
|
||||
| huawei/data_age | R | How old the data is | Seconds |
|
||||
| huawei/input_voltage | R | Input voltage | Volt (V) |
|
||||
| huawei/input_current | R | Input current | Ampere (A) |
|
||||
| huawei/input_power | R | Input power | Watt (W) |
|
||||
| huawei/output_voltage | R | Output voltage | Volt (V) |
|
||||
| huawei/output_current | R | Output current | Ampere (A) |
|
||||
| huawei/max_output_current | R | Maximum output current (set using the online limit) | Ampere (A) |
|
||||
| huawei/output_power | R | Output power | Watt (W) |
|
||||
| huawei/input_temp | R | Input air temperature | °C |
|
||||
| huawei/output_temp | R | Output air temperature | °C |
|
||||
| huawei/efficiency | R | Efficiency | Percentage |
|
||||
|
||||
## Currently supported Inverters
|
||||
* Hoymiles HM-300
|
||||
@ -177,6 +196,7 @@ Topics for 3 phases of a power meter is configurable. Given is an example for th
|
||||
* Hoymiles HM-1000
|
||||
* Hoymiles HM-1200
|
||||
* Hoymiles HM-1500
|
||||
* Solenso SOL-H350
|
||||
* Solenso SOL-H400
|
||||
* Solenso SOL-H800
|
||||
* TSUN TSOL-M350 (Maybe depending on firmware/serial number on the inverter)
|
||||
|
||||
@ -102,4 +102,6 @@ The json file can contain multiple profiles. Each profile requires a name and di
|
||||
| display.data | number | Data Pin (e.g. SDA for i2c displays) required for all displays. Use 255 for not assigned pins. |
|
||||
| display.clk | number | Clock Pin (e.g. SCL for i2c displays) required for SSD1306 and SH1106. Use 255 for not assigned pins. |
|
||||
| display.cs | number | Chip Select Pin required for PCD8544. Use 255 for not assigned pins. |
|
||||
| display.reset | number | Reset Pin required for PCD8544, optional for all other displays. Use 255 for not assigned pins. |
|
||||
| display.reset | number | Reset Pin required for PCD8544, optional for all other displays. Use 255 for not assigned pins. |
|
||||
| led.led0 | number | LED pin for network indication. Blinking = WLAN connected but NTP & MQTT (if enabled) disconnected. On = WLAN, NTP, MQTT connected. Off = Network not connected |
|
||||
| led.led1 | number | LED pin for inverter indication. On = All inverters reachable & producing. Blinking = All inverters reachable but not producing. Off = At least one inverter is not reachable. Only inverters with polling enabled are considered. |
|
||||
64
docs/DeviceProfiles/blinkyparts_esp32.json
Normal file
64
docs/DeviceProfiles/blinkyparts_esp32.json
Normal file
@ -0,0 +1,64 @@
|
||||
[
|
||||
{
|
||||
"name": "LEDs, Display",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
},
|
||||
"display": {
|
||||
"type": 3,
|
||||
"data": 21,
|
||||
"clk": 22
|
||||
},
|
||||
"led": {
|
||||
"led0": 25,
|
||||
"led1": 26
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Only Display",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
},
|
||||
"display": {
|
||||
"type": 3,
|
||||
"data": 21,
|
||||
"clk": 22
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Only LEDs",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
},
|
||||
"led": {
|
||||
"led0": 25,
|
||||
"led1": 26
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "No Output",
|
||||
"nrf24": {
|
||||
"miso": 19,
|
||||
"mosi": 23,
|
||||
"clk": 18,
|
||||
"irq": 16,
|
||||
"en": 4,
|
||||
"cs": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -5,6 +5,8 @@
|
||||
|
||||
class BatteryClass {
|
||||
public:
|
||||
uint32_t lastUpdate;
|
||||
|
||||
float chargeVoltage;
|
||||
float chargeCurrentLimitation;
|
||||
float dischargeCurrentLimitation;
|
||||
|
||||
@ -29,6 +29,13 @@
|
||||
|
||||
#define DEV_MAX_MAPPING_NAME_STRLEN 63
|
||||
|
||||
#define POWERMETER_MAX_PHASES 3
|
||||
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
|
||||
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
|
||||
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
|
||||
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
|
||||
#define POWERMETER_HTTP_TIMEOUT 1000
|
||||
|
||||
#define JSON_BUFFER_SIZE 12288
|
||||
|
||||
struct CHANNEL_CONFIG_T {
|
||||
@ -47,6 +54,15 @@ struct INVERTER_CONFIG_T {
|
||||
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
|
||||
};
|
||||
|
||||
struct POWERMETER_HTTP_PHASE_CONFIG_T {
|
||||
bool Enabled;
|
||||
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
|
||||
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
|
||||
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
|
||||
uint16_t Timeout;
|
||||
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
|
||||
};
|
||||
|
||||
struct CONFIG_T {
|
||||
uint32_t Cfg_Version;
|
||||
uint Cfg_SaveCount;
|
||||
@ -107,7 +123,9 @@ struct CONFIG_T {
|
||||
char PowerMeter_MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
|
||||
uint32_t PowerMeter_SdmBaudrate;
|
||||
uint32_t PowerMeter_SdmAddress;
|
||||
|
||||
uint32_t PowerMeter_HttpInterval;
|
||||
bool PowerMeter_HttpIndividualRequests;
|
||||
POWERMETER_HTTP_PHASE_CONFIG_T Powermeter_Http_Phase[POWERMETER_MAX_PHASES];
|
||||
|
||||
bool PowerLimiter_Enabled;
|
||||
bool PowerLimiter_SolarPassTroughEnabled;
|
||||
@ -127,6 +145,7 @@ struct CONFIG_T {
|
||||
float PowerLimiter_VoltageLoadCorrectionFactor;
|
||||
|
||||
bool Battery_Enabled;
|
||||
bool Huawei_Enabled;
|
||||
|
||||
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
|
||||
bool Security_AllowReadonly;
|
||||
|
||||
19
include/HttpPowerMeter.h
Normal file
19
include/HttpPowerMeter.h
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class HttpPowerMeterClass {
|
||||
public:
|
||||
void init();
|
||||
bool updateValues();
|
||||
float getPower(int8_t phase);
|
||||
bool httpRequest(const char* url, const char* httpHeader, const char* httpValue, uint32_t timeout,
|
||||
char* response, size_t responseSize, char* error, size_t errorSize);
|
||||
float getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float &value);
|
||||
|
||||
private:
|
||||
float power[POWERMETER_MAX_PHASES];
|
||||
};
|
||||
|
||||
extern HttpPowerMeterClass HttpPowerMeter;
|
||||
95
include/Huawei_can.h
Normal file
95
include/Huawei_can.h
Normal file
@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "SPI.h"
|
||||
#include <mcp_can.h>
|
||||
|
||||
#ifndef HUAWEI_PIN_MISO
|
||||
#define HUAWEI_PIN_MISO 12
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_MOSI
|
||||
#define HUAWEI_PIN_MOSI 13
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_SCLK
|
||||
#define HUAWEI_PIN_SCLK 26
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_IRQ
|
||||
#define HUAWEI_PIN_IRQ 25
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_CS
|
||||
#define HUAWEI_PIN_CS 15
|
||||
#endif
|
||||
|
||||
#ifndef HUAWEI_PIN_POWER
|
||||
#define HUAWEI_PIN_POWER 33
|
||||
#endif
|
||||
|
||||
#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48
|
||||
#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42
|
||||
|
||||
#define MAX_CURRENT_MULTIPLIER 20
|
||||
|
||||
#define HUAWEI_OFFLINE_VOLTAGE 0x01
|
||||
#define HUAWEI_ONLINE_VOLTAGE 0x00
|
||||
#define HUAWEI_OFFLINE_CURRENT 0x04
|
||||
#define HUAWEI_ONLINE_CURRENT 0x03
|
||||
|
||||
#define R48xx_DATA_INPUT_POWER 0x70
|
||||
#define R48xx_DATA_INPUT_FREQ 0x71
|
||||
#define R48xx_DATA_INPUT_CURRENT 0x72
|
||||
#define R48xx_DATA_OUTPUT_POWER 0x73
|
||||
#define R48xx_DATA_EFFICIENCY 0x74
|
||||
#define R48xx_DATA_OUTPUT_VOLTAGE 0x75
|
||||
#define R48xx_DATA_OUTPUT_CURRENT_MAX 0x76
|
||||
#define R48xx_DATA_INPUT_VOLTAGE 0x78
|
||||
#define R48xx_DATA_OUTPUT_TEMPERATURE 0x7F
|
||||
#define R48xx_DATA_INPUT_TEMPERATURE 0x80
|
||||
#define R48xx_DATA_OUTPUT_CURRENT 0x81
|
||||
#define R48xx_DATA_OUTPUT_CURRENT1 0x82
|
||||
|
||||
struct RectifierParameters_t {
|
||||
float input_voltage;
|
||||
float input_frequency;
|
||||
float input_current;
|
||||
float input_power;
|
||||
float input_temp;
|
||||
float efficiency;
|
||||
float output_voltage;
|
||||
float output_current;
|
||||
float max_output_current;
|
||||
float output_power;
|
||||
float output_temp;
|
||||
float amp_hour;
|
||||
};
|
||||
|
||||
class HuaweiCanClass {
|
||||
public:
|
||||
void init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power);
|
||||
void loop();
|
||||
void setValue(float in, uint8_t parameterType);
|
||||
void setPower(bool power);
|
||||
|
||||
RectifierParameters_t * get();
|
||||
uint32_t getLastUpdate();
|
||||
|
||||
private:
|
||||
void sendRequest();
|
||||
void onReceive(uint8_t* frame, uint8_t len);
|
||||
|
||||
uint32_t previousMillis;
|
||||
uint32_t lastUpdate;
|
||||
RectifierParameters_t _rp;
|
||||
|
||||
SPIClass *spi;
|
||||
MCP_CAN *CAN;
|
||||
uint8_t _huawei_irq;
|
||||
uint8_t _huawei_power;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
extern HuaweiCanClass HuaweiCan;
|
||||
35
include/Led_Single.h
Normal file
35
include/Led_Single.h
Normal file
@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "PinMapping.h"
|
||||
#include <TimeoutHelper.h>
|
||||
|
||||
#define LEDSINGLE_UPDATE_INTERVAL 2000
|
||||
|
||||
enum eLedFunction {
|
||||
CONNECTED_NETWORK,
|
||||
CONNECTED_MQTT,
|
||||
INV_REACHABLE,
|
||||
INV_PRODUCING,
|
||||
};
|
||||
|
||||
class LedSingleClass {
|
||||
public:
|
||||
LedSingleClass();
|
||||
void init();
|
||||
void loop();
|
||||
|
||||
private:
|
||||
enum class LedState_t {
|
||||
On,
|
||||
Off,
|
||||
Blink,
|
||||
};
|
||||
|
||||
LedState_t _ledState[PINMAPPING_LED_COUNT];
|
||||
TimeoutHelper _updateTimeout;
|
||||
TimeoutHelper _blinkTimeout;
|
||||
uint8_t _ledActive = 0;
|
||||
};
|
||||
|
||||
extern LedSingleClass LedSingle;
|
||||
21
include/MqttHandleHuawei.h
Normal file
21
include/MqttHandleHuawei.h
Normal file
@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "Configuration.h"
|
||||
#include <Huawei_can.h>
|
||||
#include <espMqttClient.h>
|
||||
|
||||
class MqttHandleHuaweiClass {
|
||||
public:
|
||||
void init();
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||
|
||||
uint32_t _lastPublishStats;
|
||||
uint32_t _lastPublish;
|
||||
|
||||
};
|
||||
|
||||
extern MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
@ -6,6 +6,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define PINMAPPING_FILENAME "/pin_mapping.json"
|
||||
#define PINMAPPING_LED_COUNT 2
|
||||
|
||||
#define MAPPING_NAME_STRLEN 31
|
||||
|
||||
@ -33,6 +34,13 @@ struct PinMapping_t {
|
||||
uint8_t victron_rx;
|
||||
uint8_t battery_rx;
|
||||
uint8_t battery_tx;
|
||||
uint8_t huawei_miso;
|
||||
uint8_t huawei_mosi;
|
||||
uint8_t huawei_clk;
|
||||
uint8_t huawei_irq;
|
||||
uint8_t huawei_cs;
|
||||
uint8_t huawei_power;
|
||||
int8_t led[PINMAPPING_LED_COUNT];
|
||||
};
|
||||
|
||||
class PinMappingClass {
|
||||
@ -45,7 +53,8 @@ public:
|
||||
bool isValidEthConfig();
|
||||
bool isValidVictronConfig();
|
||||
bool isValidBatteryConfig();
|
||||
|
||||
bool isValidHuaweiConfig();
|
||||
|
||||
private:
|
||||
PinMapping_t _pinMapping;
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ typedef enum {
|
||||
|
||||
typedef enum {
|
||||
EMPTY_WHEN_FULL= 0,
|
||||
EMPTY_AT_NIGTH
|
||||
EMPTY_AT_NIGHT
|
||||
} batDrainStrategy;
|
||||
|
||||
|
||||
@ -28,9 +28,10 @@ public:
|
||||
int32_t getLastRequestedPowewrLimit();
|
||||
|
||||
private:
|
||||
uint32_t _lastCommandSent;
|
||||
uint32_t _lastLoop;
|
||||
int32_t _lastRequestedPowerLimit;
|
||||
uint32_t _lastCommandSent = 0;
|
||||
uint32_t _lastLoop = 0;
|
||||
int32_t _lastRequestedPowerLimit = 0;
|
||||
uint32_t _lastLimitSetTime = 0;
|
||||
plStates _plState = STATE_DISCOVER;
|
||||
|
||||
float _powerMeter1Power;
|
||||
|
||||
@ -18,6 +18,12 @@
|
||||
|
||||
class PowerMeterClass {
|
||||
public:
|
||||
enum SOURCE {
|
||||
SOURCE_MQTT = 0,
|
||||
SOURCE_SDM1PH = 1,
|
||||
SOURCE_SDM3PH = 2,
|
||||
SOURCE_HTTP = 3,
|
||||
};
|
||||
void init();
|
||||
void mqtt();
|
||||
void loop();
|
||||
@ -27,6 +33,8 @@ public:
|
||||
|
||||
private:
|
||||
uint32_t _interval;
|
||||
uint32_t _lastPowerMeterCheck;
|
||||
// Used in Power limiter for safety check
|
||||
uint32_t _lastPowerMeterUpdate;
|
||||
|
||||
float _powerMeter1Power = 0.0;
|
||||
|
||||
@ -25,6 +25,10 @@
|
||||
#include "WebApi_ws_live.h"
|
||||
#include "WebApi_ws_vedirect_live.h"
|
||||
#include "WebApi_vedirect.h"
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "WebApi_ws_Pylontech.h"
|
||||
#include "WebApi_Pylontech.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiClass {
|
||||
@ -66,6 +70,11 @@ private:
|
||||
WebApiWsLiveClass _webApiWsLive;
|
||||
WebApiWsVedirectLiveClass _webApiWsVedirectLive;
|
||||
WebApiVedirectClass _webApiVedirect;
|
||||
WebApiHuaweiClass _webApiHuaweiClass;
|
||||
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
|
||||
WebApiPylontechClass _webApiPylontechClass;
|
||||
WebApiWsPylontechLiveClass _webApiWsPylontechLive;
|
||||
|
||||
};
|
||||
|
||||
extern WebApiClass WebApi;
|
||||
19
include/WebApi_Huawei.h
Normal file
19
include/WebApi_Huawei.h
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiHuaweiClass {
|
||||
public:
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
void getJsonData(JsonObject& root);
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onPost(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
17
include/WebApi_Pylontech.h
Normal file
17
include/WebApi_Pylontech.h
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
class WebApiPylontechClass {
|
||||
public:
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
void getJsonData(JsonObject& root);
|
||||
|
||||
private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
@ -13,6 +13,7 @@ private:
|
||||
void onStatus(AsyncWebServerRequest* request);
|
||||
void onAdminGet(AsyncWebServerRequest* request);
|
||||
void onAdminPost(AsyncWebServerRequest* request);
|
||||
void onTestHttpRequest(AsyncWebServerRequest* request);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
};
|
||||
};
|
||||
|
||||
24
include/WebApi_ws_Huawei.h
Normal file
24
include/WebApi_ws_Huawei.h
Normal file
@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
//#include <HuaweiFrameHandler.h>
|
||||
|
||||
class WebApiWsHuaweiLiveClass {
|
||||
public:
|
||||
WebApiWsHuaweiLiveClass();
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
};
|
||||
23
include/WebApi_ws_Pylontech.h
Normal file
23
include/WebApi_ws_Pylontech.h
Normal file
@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include "ArduinoJson.h"
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class WebApiWsPylontechLiveClass {
|
||||
public:
|
||||
WebApiWsPylontechLiveClass();
|
||||
void init(AsyncWebServer* server);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
void generateJsonResponse(JsonVariant& root);
|
||||
void onLivedataStatus(AsyncWebServerRequest* request);
|
||||
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
|
||||
AsyncWebServer* _server;
|
||||
AsyncWebSocket _ws;
|
||||
|
||||
uint32_t _lastWsCleanup = 0;
|
||||
uint32_t _lastUpdateCheck = 0;
|
||||
};
|
||||
@ -117,3 +117,5 @@
|
||||
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
|
||||
|
||||
#define BATTERY_ENABLED false
|
||||
|
||||
#define HUAWEI_ENABLED false
|
||||
|
||||
@ -35,7 +35,7 @@ void HoymilesClass::loop()
|
||||
if (_radio->isIdle()) {
|
||||
std::shared_ptr<InverterAbstract> iv = getInverterByPos(inverterPos);
|
||||
if (iv != nullptr) {
|
||||
_messageOutput->print(F("Fetch inverter: "));
|
||||
_messageOutput->print("Fetch inverter: ");
|
||||
_messageOutput->println(iv->serial(), HEX);
|
||||
|
||||
iv->sendStatsRequest(_radio.get());
|
||||
@ -54,19 +54,19 @@ void HoymilesClass::loop()
|
||||
|
||||
// Set limit if required
|
||||
if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) {
|
||||
_messageOutput->println(F("Resend ActivePowerControl"));
|
||||
_messageOutput->println("Resend ActivePowerControl");
|
||||
iv->resendActivePowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Set power status if required
|
||||
if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) {
|
||||
_messageOutput->println(F("Resend PowerCommand"));
|
||||
_messageOutput->println("Resend PowerCommand");
|
||||
iv->resendPowerControlRequest(_radio.get());
|
||||
}
|
||||
|
||||
// Fetch dev info (but first fetch stats)
|
||||
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) {
|
||||
_messageOutput->println(F("Request device info"));
|
||||
_messageOutput->println("Request device info");
|
||||
iv->sendDevInfoRequest(_radio.get());
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,9 +25,9 @@ void HoymilesRadio::init(SPIClass* initialisedSpiBus, uint8_t pinCE, uint8_t pin
|
||||
_radio->setRetries(0, 0);
|
||||
_radio->maskIRQ(true, true, false); // enable only receiving interrupts
|
||||
if (_radio->isChipConnected()) {
|
||||
Hoymiles.getMessageOutput()->println(F("Connection successful"));
|
||||
Hoymiles.getMessageOutput()->println("Connection successful");
|
||||
} else {
|
||||
Hoymiles.getMessageOutput()->println(F("Connection error!!"));
|
||||
Hoymiles.getMessageOutput()->println("Connection error!!");
|
||||
}
|
||||
|
||||
attachInterrupt(digitalPinToInterrupt(pinIRQ), std::bind(&HoymilesRadio::handleIntr, this), FALLING);
|
||||
@ -44,7 +44,7 @@ void HoymilesRadio::loop()
|
||||
}
|
||||
|
||||
if (_packetReceived) {
|
||||
Hoymiles.getMessageOutput()->println(F("Interrupt received"));
|
||||
Hoymiles.getMessageOutput()->println("Interrupt received");
|
||||
while (_radio->available()) {
|
||||
if (!(_rxBuffer.size() > FRAGMENT_BUFFER_SIZE)) {
|
||||
fragment_t f;
|
||||
@ -56,7 +56,7 @@ void HoymilesRadio::loop()
|
||||
_radio->read(f.fragment, f.len);
|
||||
_rxBuffer.push(f);
|
||||
} else {
|
||||
Hoymiles.getMessageOutput()->println(F("Buffer full"));
|
||||
Hoymiles.getMessageOutput()->println("Buffer full");
|
||||
_radio->flush_rx();
|
||||
}
|
||||
}
|
||||
@ -76,11 +76,11 @@ void HoymilesRadio::loop()
|
||||
dumpBuf(buf, f.fragment, f.len);
|
||||
inv->addRxFragment(f.fragment, f.len);
|
||||
} else {
|
||||
Hoymiles.getMessageOutput()->println(F("Inverter Not found!"));
|
||||
Hoymiles.getMessageOutput()->println("Inverter Not found!");
|
||||
}
|
||||
|
||||
} else {
|
||||
Hoymiles.getMessageOutput()->println(F("Frame kaputt"));
|
||||
Hoymiles.getMessageOutput()->println("Frame kaputt");
|
||||
}
|
||||
|
||||
// Remove paket from buffer even it was corrupted
|
||||
@ -89,46 +89,46 @@ void HoymilesRadio::loop()
|
||||
}
|
||||
|
||||
if (_busyFlag && _rxTimeout.occured()) {
|
||||
Hoymiles.getMessageOutput()->println(F("RX Period End"));
|
||||
Hoymiles.getMessageOutput()->println("RX Period End");
|
||||
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(_commandQueue.front().get()->getTargetAddress());
|
||||
|
||||
if (nullptr != inv) {
|
||||
CommandAbstract* cmd = _commandQueue.front().get();
|
||||
uint8_t verifyResult = inv->verifyAllFragments(cmd);
|
||||
if (verifyResult == FRAGMENT_ALL_MISSING_RESEND) {
|
||||
Hoymiles.getMessageOutput()->println(F("Nothing received, resend whole request"));
|
||||
Hoymiles.getMessageOutput()->println("Nothing received, resend whole request");
|
||||
sendLastPacketAgain();
|
||||
|
||||
} else if (verifyResult == FRAGMENT_ALL_MISSING_TIMEOUT) {
|
||||
Hoymiles.getMessageOutput()->println(F("Nothing received, resend count exeeded"));
|
||||
Hoymiles.getMessageOutput()->println("Nothing received, resend count exeeded");
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
|
||||
} else if (verifyResult == FRAGMENT_RETRANSMIT_TIMEOUT) {
|
||||
Hoymiles.getMessageOutput()->println(F("Retransmit timeout"));
|
||||
Hoymiles.getMessageOutput()->println("Retransmit timeout");
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
|
||||
} else if (verifyResult == FRAGMENT_HANDLE_ERROR) {
|
||||
Hoymiles.getMessageOutput()->println(F("Packet handling error"));
|
||||
Hoymiles.getMessageOutput()->println("Packet handling error");
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
|
||||
} else if (verifyResult > 0) {
|
||||
// Perform Retransmit
|
||||
Hoymiles.getMessageOutput()->print(F("Request retransmit: "));
|
||||
Hoymiles.getMessageOutput()->print("Request retransmit: ");
|
||||
Hoymiles.getMessageOutput()->println(verifyResult);
|
||||
sendRetransmitPacket(verifyResult);
|
||||
|
||||
} else {
|
||||
// Successful received all packages
|
||||
Hoymiles.getMessageOutput()->println(F("Success"));
|
||||
Hoymiles.getMessageOutput()->println("Success");
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
}
|
||||
} else {
|
||||
// If inverter was not found, assume the command is invalid
|
||||
Hoymiles.getMessageOutput()->println(F("RX: Invalid inverter found"));
|
||||
Hoymiles.getMessageOutput()->println("RX: Invalid inverter found");
|
||||
_commandQueue.pop();
|
||||
_busyFlag = false;
|
||||
}
|
||||
@ -142,7 +142,7 @@ void HoymilesRadio::loop()
|
||||
inv->clearRxFragmentBuffer();
|
||||
sendEsbPacket(cmd);
|
||||
} else {
|
||||
Hoymiles.getMessageOutput()->println(F("TX: Invalid inverter found"));
|
||||
Hoymiles.getMessageOutput()->println("TX: Invalid inverter found");
|
||||
_commandQueue.pop();
|
||||
}
|
||||
}
|
||||
@ -252,11 +252,11 @@ void HoymilesRadio::sendEsbPacket(CommandAbstract* cmd)
|
||||
openWritingPipe(s);
|
||||
_radio->setRetries(3, 15);
|
||||
|
||||
Hoymiles.getMessageOutput()->print(F("TX "));
|
||||
Hoymiles.getMessageOutput()->print("TX ");
|
||||
Hoymiles.getMessageOutput()->print(cmd->getCommandName());
|
||||
Hoymiles.getMessageOutput()->print(F(" Channel: "));
|
||||
Hoymiles.getMessageOutput()->print(" Channel: ");
|
||||
Hoymiles.getMessageOutput()->print(_radio->getChannel());
|
||||
Hoymiles.getMessageOutput()->print(F(" --> "));
|
||||
Hoymiles.getMessageOutput()->print(" --> ");
|
||||
cmd->dumpDataPayload(Hoymiles.getMessageOutput());
|
||||
_radio->write(cmd->getDataPayload(), cmd->getDataSize());
|
||||
|
||||
@ -294,5 +294,5 @@ void HoymilesRadio::dumpBuf(const char* info, uint8_t buf[], uint8_t len)
|
||||
for (uint8_t i = 0; i < len; i++) {
|
||||
Hoymiles.getMessageOutput()->printf("%02X ", buf[i]);
|
||||
}
|
||||
Hoymiles.getMessageOutput()->println(F(""));
|
||||
Hoymiles.getMessageOutput()->println("");
|
||||
}
|
||||
@ -29,7 +29,7 @@ bool HM_1CH::isValidSerial(uint64_t serial)
|
||||
|
||||
String HM_1CH::typeName()
|
||||
{
|
||||
return F("HM-300, HM-350, HM-400");
|
||||
return "HM-300, HM-350, HM-400";
|
||||
}
|
||||
|
||||
const std::list<byteAssign_t>* HM_1CH::getByteAssignment()
|
||||
|
||||
@ -30,7 +30,7 @@ bool HM_2CH::isValidSerial(uint64_t serial)
|
||||
|
||||
String HM_2CH::typeName()
|
||||
{
|
||||
return F("HM-600, HM-700, HM-800");
|
||||
return "HM-600, HM-700, HM-800";
|
||||
}
|
||||
|
||||
const std::list<byteAssign_t>* HM_2CH::getByteAssignment()
|
||||
|
||||
@ -29,7 +29,7 @@ bool HM_4CH::isValidSerial(uint64_t serial)
|
||||
|
||||
String HM_4CH::typeName()
|
||||
{
|
||||
return F("HM-1000, HM-1200, HM-1500");
|
||||
return "HM-1000, HM-1200, HM-1500";
|
||||
}
|
||||
|
||||
const std::list<byteAssign_t>* HM_4CH::getByteAssignment()
|
||||
|
||||
@ -169,7 +169,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
{
|
||||
// All missing
|
||||
if (_rxFragmentLastPacketId == 0) {
|
||||
Hoymiles.getMessageOutput()->println(F("All missing"));
|
||||
Hoymiles.getMessageOutput()->println("All missing");
|
||||
if (cmd->getSendCount() <= MAX_RESEND_COUNT) {
|
||||
return FRAGMENT_ALL_MISSING_RESEND;
|
||||
} else {
|
||||
@ -178,9 +178,9 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
}
|
||||
}
|
||||
|
||||
// Last fragment is missing (thte one with 0x80)
|
||||
// Last fragment is missing (the one with 0x80)
|
||||
if (_rxFragmentMaxPacketId == 0) {
|
||||
Hoymiles.getMessageOutput()->println(F("Last missing"));
|
||||
Hoymiles.getMessageOutput()->println("Last missing");
|
||||
if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) {
|
||||
return _rxFragmentLastPacketId + 1;
|
||||
} else {
|
||||
@ -192,7 +192,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract* cmd)
|
||||
// Middle fragment is missing
|
||||
for (uint8_t i = 0; i < _rxFragmentMaxPacketId - 1; i++) {
|
||||
if (!_rxFragmentBuffer[i].wasReceived) {
|
||||
Hoymiles.getMessageOutput()->println(F("Middle missing"));
|
||||
Hoymiles.getMessageOutput()->println("Middle missing");
|
||||
if (_rxFragmentRetransmitCnt++ < MAX_RETRANSMIT_COUNT) {
|
||||
return i + 1;
|
||||
} else {
|
||||
|
||||
@ -64,214 +64,214 @@ void AlarmLogParser::getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry)
|
||||
|
||||
switch (entry->MessageId) {
|
||||
case 1:
|
||||
entry->Message = F("Inverter start");
|
||||
entry->Message = "Inverter start";
|
||||
break;
|
||||
case 2:
|
||||
entry->Message = F("DTU command failed");
|
||||
entry->Message = "DTU command failed";
|
||||
break;
|
||||
case 121:
|
||||
entry->Message = F("Over temperature protection");
|
||||
entry->Message = "Over temperature protection";
|
||||
break;
|
||||
case 124:
|
||||
entry->Message = F("Shut down by remote control");
|
||||
entry->Message = "Shut down by remote control";
|
||||
break;
|
||||
case 125:
|
||||
entry->Message = F("Grid configuration parameter error");
|
||||
entry->Message = "Grid configuration parameter error";
|
||||
break;
|
||||
case 126:
|
||||
entry->Message = F("Software error code 126");
|
||||
entry->Message = "Software error code 126";
|
||||
break;
|
||||
case 127:
|
||||
entry->Message = F("Firmware error");
|
||||
entry->Message = "Firmware error";
|
||||
break;
|
||||
case 128:
|
||||
entry->Message = F("Software error code 128");
|
||||
entry->Message = "Software error code 128";
|
||||
break;
|
||||
case 129:
|
||||
entry->Message = F("Abnormal bias");
|
||||
entry->Message = "Abnormal bias";
|
||||
break;
|
||||
case 130:
|
||||
entry->Message = F("Offline");
|
||||
entry->Message = "Offline";
|
||||
break;
|
||||
case 141:
|
||||
entry->Message = F("Grid: Grid overvoltage");
|
||||
entry->Message = "Grid: Grid overvoltage";
|
||||
break;
|
||||
case 142:
|
||||
entry->Message = F("Grid: 10 min value grid overvoltage");
|
||||
entry->Message = "Grid: 10 min value grid overvoltage";
|
||||
break;
|
||||
case 143:
|
||||
entry->Message = F("Grid: Grid undervoltage");
|
||||
entry->Message = "Grid: Grid undervoltage";
|
||||
break;
|
||||
case 144:
|
||||
entry->Message = F("Grid: Grid overfrequency");
|
||||
entry->Message = "Grid: Grid overfrequency";
|
||||
break;
|
||||
case 145:
|
||||
entry->Message = F("Grid: Grid underfrequency");
|
||||
entry->Message = "Grid: Grid underfrequency";
|
||||
break;
|
||||
case 146:
|
||||
entry->Message = F("Grid: Rapid grid frequency change rate");
|
||||
entry->Message = "Grid: Rapid grid frequency change rate";
|
||||
break;
|
||||
case 147:
|
||||
entry->Message = F("Grid: Power grid outage");
|
||||
entry->Message = "Grid: Power grid outage";
|
||||
break;
|
||||
case 148:
|
||||
entry->Message = F("Grid: Grid disconnection");
|
||||
entry->Message = "Grid: Grid disconnection";
|
||||
break;
|
||||
case 149:
|
||||
entry->Message = F("Grid: Island detected");
|
||||
entry->Message = "Grid: Island detected";
|
||||
break;
|
||||
case 205:
|
||||
entry->Message = F("MPPT-A: Input overvoltage");
|
||||
entry->Message = "MPPT-A: Input overvoltage";
|
||||
break;
|
||||
case 206:
|
||||
entry->Message = F("MPPT-B: Input overvoltage");
|
||||
entry->Message = "MPPT-B: Input overvoltage";
|
||||
break;
|
||||
case 207:
|
||||
entry->Message = F("MPPT-A: Input undervoltage");
|
||||
entry->Message = "MPPT-A: Input undervoltage";
|
||||
break;
|
||||
case 208:
|
||||
entry->Message = F("MPPT-B: Input undervoltage");
|
||||
entry->Message = "MPPT-B: Input undervoltage";
|
||||
break;
|
||||
case 209:
|
||||
entry->Message = F("PV-1: No input");
|
||||
entry->Message = "PV-1: No input";
|
||||
break;
|
||||
case 210:
|
||||
entry->Message = F("PV-2: No input");
|
||||
entry->Message = "PV-2: No input";
|
||||
break;
|
||||
case 211:
|
||||
entry->Message = F("PV-3: No input");
|
||||
entry->Message = "PV-3: No input";
|
||||
break;
|
||||
case 212:
|
||||
entry->Message = F("PV-4: No input");
|
||||
entry->Message = "PV-4: No input";
|
||||
break;
|
||||
case 213:
|
||||
entry->Message = F("MPPT-A: PV-1 & PV-2 abnormal wiring");
|
||||
entry->Message = "MPPT-A: PV-1 & PV-2 abnormal wiring";
|
||||
break;
|
||||
case 214:
|
||||
entry->Message = F("MPPT-B: PV-3 & PV-4 abnormal wiring");
|
||||
entry->Message = "MPPT-B: PV-3 & PV-4 abnormal wiring";
|
||||
break;
|
||||
case 215:
|
||||
entry->Message = F("PV-1: Input overvoltage");
|
||||
entry->Message = "PV-1: Input overvoltage";
|
||||
break;
|
||||
case 216:
|
||||
entry->Message = F("PV-1: Input undervoltage");
|
||||
entry->Message = "PV-1: Input undervoltage";
|
||||
break;
|
||||
case 217:
|
||||
entry->Message = F("PV-2: Input overvoltage");
|
||||
entry->Message = "PV-2: Input overvoltage";
|
||||
break;
|
||||
case 218:
|
||||
entry->Message = F("PV-2: Input undervoltage");
|
||||
entry->Message = "PV-2: Input undervoltage";
|
||||
break;
|
||||
case 219:
|
||||
entry->Message = F("PV-3: Input overvoltage");
|
||||
entry->Message = "PV-3: Input overvoltage";
|
||||
break;
|
||||
case 220:
|
||||
entry->Message = F("PV-3: Input undervoltage");
|
||||
entry->Message = "PV-3: Input undervoltage";
|
||||
break;
|
||||
case 221:
|
||||
entry->Message = F("PV-4: Input overvoltage");
|
||||
entry->Message = "PV-4: Input overvoltage";
|
||||
break;
|
||||
case 222:
|
||||
entry->Message = F("PV-4: Input undervoltage");
|
||||
entry->Message = "PV-4: Input undervoltage";
|
||||
break;
|
||||
case 301:
|
||||
entry->Message = F("Hardware error code 301");
|
||||
entry->Message = "Hardware error code 301";
|
||||
break;
|
||||
case 302:
|
||||
entry->Message = F("Hardware error code 302");
|
||||
entry->Message = "Hardware error code 302";
|
||||
break;
|
||||
case 303:
|
||||
entry->Message = F("Hardware error code 303");
|
||||
entry->Message = "Hardware error code 303";
|
||||
break;
|
||||
case 304:
|
||||
entry->Message = F("Hardware error code 304");
|
||||
entry->Message = "Hardware error code 304";
|
||||
break;
|
||||
case 305:
|
||||
entry->Message = F("Hardware error code 305");
|
||||
entry->Message = "Hardware error code 305";
|
||||
break;
|
||||
case 306:
|
||||
entry->Message = F("Hardware error code 306");
|
||||
entry->Message = "Hardware error code 306";
|
||||
break;
|
||||
case 307:
|
||||
entry->Message = F("Hardware error code 307");
|
||||
entry->Message = "Hardware error code 307";
|
||||
break;
|
||||
case 308:
|
||||
entry->Message = F("Hardware error code 308");
|
||||
entry->Message = "Hardware error code 308";
|
||||
break;
|
||||
case 309:
|
||||
entry->Message = F("Hardware error code 309");
|
||||
entry->Message = "Hardware error code 309";
|
||||
break;
|
||||
case 310:
|
||||
entry->Message = F("Hardware error code 310");
|
||||
entry->Message = "Hardware error code 310";
|
||||
break;
|
||||
case 311:
|
||||
entry->Message = F("Hardware error code 311");
|
||||
entry->Message = "Hardware error code 311";
|
||||
break;
|
||||
case 312:
|
||||
entry->Message = F("Hardware error code 312");
|
||||
entry->Message = "Hardware error code 312";
|
||||
break;
|
||||
case 313:
|
||||
entry->Message = F("Hardware error code 313");
|
||||
entry->Message = "Hardware error code 313";
|
||||
break;
|
||||
case 314:
|
||||
entry->Message = F("Hardware error code 314");
|
||||
entry->Message = "Hardware error code 314";
|
||||
break;
|
||||
case 5041:
|
||||
entry->Message = F("Error code-04 Port 1");
|
||||
entry->Message = "Error code-04 Port 1";
|
||||
break;
|
||||
case 5042:
|
||||
entry->Message = F("Error code-04 Port 2");
|
||||
entry->Message = "Error code-04 Port 2";
|
||||
break;
|
||||
case 5043:
|
||||
entry->Message = F("Error code-04 Port 3");
|
||||
entry->Message = "Error code-04 Port 3";
|
||||
break;
|
||||
case 5044:
|
||||
entry->Message = F("Error code-04 Port 4");
|
||||
entry->Message = "Error code-04 Port 4";
|
||||
break;
|
||||
case 5051:
|
||||
entry->Message = F("PV Input 1 Overvoltage/Undervoltage");
|
||||
entry->Message = "PV Input 1 Overvoltage/Undervoltage";
|
||||
break;
|
||||
case 5052:
|
||||
entry->Message = F("PV Input 2 Overvoltage/Undervoltage");
|
||||
entry->Message = "PV Input 2 Overvoltage/Undervoltage";
|
||||
break;
|
||||
case 5053:
|
||||
entry->Message = F("PV Input 3 Overvoltage/Undervoltage");
|
||||
entry->Message = "PV Input 3 Overvoltage/Undervoltage";
|
||||
break;
|
||||
case 5054:
|
||||
entry->Message = F("PV Input 4 Overvoltage/Undervoltage");
|
||||
entry->Message = "PV Input 4 Overvoltage/Undervoltage";
|
||||
break;
|
||||
case 5060:
|
||||
entry->Message = F("Abnormal bias");
|
||||
entry->Message = "Abnormal bias";
|
||||
break;
|
||||
case 5070:
|
||||
entry->Message = F("Over temperature protection");
|
||||
entry->Message = "Over temperature protection";
|
||||
break;
|
||||
case 5080:
|
||||
entry->Message = F("Grid Overvoltage/Undervoltage");
|
||||
entry->Message = "Grid Overvoltage/Undervoltage";
|
||||
break;
|
||||
case 5090:
|
||||
entry->Message = F("Grid Overfrequency/Underfrequency");
|
||||
entry->Message = "Grid Overfrequency/Underfrequency";
|
||||
break;
|
||||
case 5100:
|
||||
entry->Message = F("Island detected");
|
||||
entry->Message = "Island detected";
|
||||
break;
|
||||
case 5120:
|
||||
entry->Message = F("EEPROM reading and writing error");
|
||||
entry->Message = "EEPROM reading and writing error";
|
||||
break;
|
||||
case 5150:
|
||||
entry->Message = F("10 min value grid overvoltage");
|
||||
entry->Message = "10 min value grid overvoltage";
|
||||
break;
|
||||
case 5200:
|
||||
entry->Message = F("Firmware error");
|
||||
entry->Message = "Firmware error";
|
||||
break;
|
||||
case 8310:
|
||||
entry->Message = F("Shut down");
|
||||
entry->Message = "Shut down";
|
||||
break;
|
||||
case 9000:
|
||||
entry->Message = F("Microinverter is suspected of being stolen");
|
||||
entry->Message = "Microinverter is suspected of being stolen";
|
||||
break;
|
||||
default:
|
||||
entry->Message = F("Unknown");
|
||||
entry->Message = "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch
|
||||
}
|
||||
|
||||
result /= static_cast<float>(div);
|
||||
if (setting != NULL) {
|
||||
if (setting != NULL && _statisticLength > 0) {
|
||||
result += setting->offset;
|
||||
}
|
||||
return result;
|
||||
|
||||
@ -29,58 +29,58 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id)
|
||||
|
||||
switch (reason) {
|
||||
case 1:
|
||||
reason_str = F("Vbat power on reset");
|
||||
reason_str = "Vbat power on reset";
|
||||
break;
|
||||
case 3:
|
||||
reason_str = F("Software reset digital core");
|
||||
reason_str = "Software reset digital core";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 4:
|
||||
reason_str = F("Legacy watch dog reset digital core");
|
||||
reason_str = "Legacy watch dog reset digital core";
|
||||
break;
|
||||
#endif
|
||||
case 5:
|
||||
reason_str = F("Deep Sleep reset digital core");
|
||||
reason_str = "Deep Sleep reset digital core";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 6:
|
||||
reason_str = F("Reset by SLC module, reset digital core");
|
||||
reason_str = "Reset by SLC module, reset digital core";
|
||||
break;
|
||||
#endif
|
||||
case 7:
|
||||
reason_str = F("Timer Group0 Watch dog reset digital core");
|
||||
reason_str = "Timer Group0 Watch dog reset digital core";
|
||||
break;
|
||||
case 8:
|
||||
reason_str = F("Timer Group1 Watch dog reset digital core");
|
||||
reason_str = "Timer Group1 Watch dog reset digital core";
|
||||
break;
|
||||
case 9:
|
||||
reason_str = F("RTC Watch dog Reset digital core");
|
||||
reason_str = "RTC Watch dog Reset digital core";
|
||||
break;
|
||||
case 10:
|
||||
reason_str = F("Instrusion tested to reset CPU");
|
||||
reason_str = "Instrusion tested to reset CPU";
|
||||
break;
|
||||
case 11:
|
||||
reason_str = F("Time Group reset CPU");
|
||||
reason_str = "Time Group reset CPU";
|
||||
break;
|
||||
case 12:
|
||||
reason_str = F("Software reset CPU");
|
||||
reason_str = "Software reset CPU";
|
||||
break;
|
||||
case 13:
|
||||
reason_str = F("RTC Watch dog Reset CPU");
|
||||
reason_str = "RTC Watch dog Reset CPU";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 14:
|
||||
reason_str = F("for APP CPU, reset by PRO CPU");
|
||||
reason_str = "for APP CPU, reset by PRO CPU";
|
||||
break;
|
||||
#endif
|
||||
case 15:
|
||||
reason_str = F("Reset when the vdd voltage is not stable");
|
||||
reason_str = "Reset when the vdd voltage is not stable";
|
||||
break;
|
||||
case 16:
|
||||
reason_str = F("RTC Watch dog reset digital core and rtc module");
|
||||
reason_str = "RTC Watch dog reset digital core and rtc module";
|
||||
break;
|
||||
default:
|
||||
reason_str = F("NO_MEAN");
|
||||
reason_str = "NO_MEAN";
|
||||
}
|
||||
|
||||
return reason_str;
|
||||
@ -95,58 +95,58 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id)
|
||||
|
||||
switch (reason) {
|
||||
case 1:
|
||||
reason_str = F("POWERON_RESET");
|
||||
reason_str = "POWERON_RESET";
|
||||
break;
|
||||
case 3:
|
||||
reason_str = F("SW_RESET");
|
||||
reason_str = "SW_RESET";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 4:
|
||||
reason_str = F("OWDT_RESET");
|
||||
reason_str = "OWDT_RESET";
|
||||
break;
|
||||
#endif
|
||||
case 5:
|
||||
reason_str = F("DEEPSLEEP_RESET");
|
||||
reason_str = "DEEPSLEEP_RESET";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 6:
|
||||
reason_str = F("SDIO_RESET");
|
||||
reason_str = "SDIO_RESET";
|
||||
break;
|
||||
#endif
|
||||
case 7:
|
||||
reason_str = F("TG0WDT_SYS_RESET");
|
||||
reason_str = "TG0WDT_SYS_RESET";
|
||||
break;
|
||||
case 8:
|
||||
reason_str = F("TG1WDT_SYS_RESET");
|
||||
reason_str = "TG1WDT_SYS_RESET";
|
||||
break;
|
||||
case 9:
|
||||
reason_str = F("RTCWDT_SYS_RESET");
|
||||
reason_str = "RTCWDT_SYS_RESET";
|
||||
break;
|
||||
case 10:
|
||||
reason_str = F("INTRUSION_RESET");
|
||||
reason_str = "INTRUSION_RESET";
|
||||
break;
|
||||
case 11:
|
||||
reason_str = F("TGWDT_CPU_RESET");
|
||||
reason_str = "TGWDT_CPU_RESET";
|
||||
break;
|
||||
case 12:
|
||||
reason_str = F("SW_CPU_RESET");
|
||||
reason_str = "SW_CPU_RESET";
|
||||
break;
|
||||
case 13:
|
||||
reason_str = F("RTCWDT_CPU_RESET");
|
||||
reason_str = "RTCWDT_CPU_RESET";
|
||||
break;
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
case 14:
|
||||
reason_str = F("EXT_CPU_RESET");
|
||||
reason_str = "EXT_CPU_RESET";
|
||||
break;
|
||||
#endif
|
||||
case 15:
|
||||
reason_str = F("RTCWDT_BROWN_OUT_RESET");
|
||||
reason_str = "RTCWDT_BROWN_OUT_RESET";
|
||||
break;
|
||||
case 16:
|
||||
reason_str = F("RTCWDT_RTC_RESET");
|
||||
reason_str = "RTCWDT_RTC_RESET";
|
||||
break;
|
||||
default:
|
||||
reason_str = F("NO_MEAN");
|
||||
reason_str = "NO_MEAN";
|
||||
}
|
||||
|
||||
return reason_str;
|
||||
|
||||
@ -22,6 +22,11 @@ void TimeoutHelper::extend(uint32_t ms)
|
||||
timeout += ms;
|
||||
}
|
||||
|
||||
void TimeoutHelper::reset()
|
||||
{
|
||||
startMillis = millis();
|
||||
}
|
||||
|
||||
bool TimeoutHelper::occured()
|
||||
{
|
||||
return millis() > (startMillis + timeout);
|
||||
|
||||
@ -8,6 +8,7 @@ public:
|
||||
TimeoutHelper();
|
||||
void set(uint32_t ms);
|
||||
void extend(uint32_t ms);
|
||||
void reset();
|
||||
bool occured();
|
||||
|
||||
private:
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
|
||||
|
||||
typedef struct {
|
||||
uint16_t PID; // pruduct id
|
||||
uint16_t PID; // product id
|
||||
char SER[VE_MAX_VALUE_LEN]; // serial number
|
||||
char FW[VE_MAX_VALUE_LEN]; // firmware release number
|
||||
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
|
||||
|
||||
@ -15,7 +15,7 @@ extra_configs =
|
||||
|
||||
[env]
|
||||
framework = arduino
|
||||
platform = espressif32@>=6.0.1
|
||||
platform = espressif32@>=6.1.0
|
||||
|
||||
build_flags =
|
||||
-DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz
|
||||
@ -32,6 +32,8 @@ lib_deps =
|
||||
nrf24/RF24 @ ^1.4.5
|
||||
olikraus/U8g2 @ ^2.34.16
|
||||
buelowp/sunset @ ^1.1.7
|
||||
https://github.com/coryjfowler/MCP_CAN_lib
|
||||
mobizt/FirebaseJson @ ^3.0.6
|
||||
|
||||
extra_scripts =
|
||||
pre:auto_firmware_version.py
|
||||
@ -57,9 +59,15 @@ build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_CE=4
|
||||
-DHOYMILES_PIN_CS=5
|
||||
-DVICTRON_PIN_TX=21
|
||||
-DVICTRON_PIN_RX=22
|
||||
-DVICTRON_PIN_RX=22
|
||||
-DPYLONTECH_PIN_RX=27
|
||||
-DPYLONTECH_PIN_TX=14
|
||||
-DHUAWEI_PIN_MISO=12
|
||||
-DHUAWEI_PIN_MOSI=13
|
||||
-DHUAWEI_PIN_SCLK=26
|
||||
-DHUAWEI_PIN_IRQ=25
|
||||
-DHUAWEI_PIN_CS=15
|
||||
-DHUAWEI_PIN_POWER=33
|
||||
|
||||
|
||||
[env:olimex_esp32_poe]
|
||||
@ -88,7 +96,7 @@ build_flags = ${env.build_flags}
|
||||
-DOPENDTU_ETHERNET
|
||||
|
||||
|
||||
[env:d1 mini esp32]
|
||||
[env:d1_mini_esp32]
|
||||
board = wemos_d1_mini32
|
||||
build_flags =
|
||||
${env.build_flags}
|
||||
@ -102,6 +110,12 @@ build_flags =
|
||||
-DVICTRON_PIN_RX=22
|
||||
-DPYLONTECH_PIN_RX=27
|
||||
-DPYLONTECH_PIN_TX=14
|
||||
-DHUAWEI_PIN_MISO=12
|
||||
-DHUAWEI_PIN_MOSI=13
|
||||
-DHUAWEI_PIN_SCLK=26
|
||||
-DHUAWEI_PIN_IRQ=25
|
||||
-DHUAWEI_PIN_CS=15
|
||||
-DHUAWEI_PIN_POWER=33
|
||||
|
||||
[env:wt32_eth01]
|
||||
; http://www.wireless-tag.com/portfolio/wt32-eth01/
|
||||
@ -167,4 +181,4 @@ build_flags = ${env.build_flags}
|
||||
-DHOYMILES_PIN_SCLK=12
|
||||
-DHOYMILES_PIN_CS=10
|
||||
-DHOYMILES_PIN_IRQ=4
|
||||
-DHOYMILES_PIN_CE=5
|
||||
-DHOYMILES_PIN_CE=5
|
||||
|
||||
@ -32,5 +32,10 @@
|
||||
; -DHOYMILES_PIN_CS=6
|
||||
; -DVICTRON_PIN_TX=21
|
||||
; -DVICTRON_PIN_RX=22
|
||||
; -DHUAWEI_PIN_MISO=12
|
||||
; -DHUAWEI_PIN_MOSI=13
|
||||
; -DHUAWEI_PIN_SCLK=26
|
||||
; -DHUAWEI_PIN_IRQ=25
|
||||
; -DHUAWEI_PIN_CS=15
|
||||
;monitor_port = /dev/ttyACM0
|
||||
;upload_port = /dev/ttyACM0
|
||||
|
||||
@ -124,6 +124,19 @@ bool ConfigurationClass::write()
|
||||
powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter_MqttTopicPowerMeter3;
|
||||
powermeter["sdmbaudrate"] = config.PowerMeter_SdmBaudrate;
|
||||
powermeter["sdmaddress"] = config.PowerMeter_SdmAddress;
|
||||
powermeter["http_individual_requests"] = config.PowerMeter_HttpIndividualRequests;
|
||||
|
||||
JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases");
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
JsonObject powermeter_phase = powermeter_http_phases.createNestedObject();
|
||||
|
||||
powermeter_phase["enabled"] = config.Powermeter_Http_Phase[i].Enabled;
|
||||
powermeter_phase["url"] = config.Powermeter_Http_Phase[i].Url;
|
||||
powermeter_phase["header_key"] = config.Powermeter_Http_Phase[i].HeaderKey;
|
||||
powermeter_phase["header_value"] = config.Powermeter_Http_Phase[i].HeaderValue;
|
||||
powermeter_phase["timeout"] = config.Powermeter_Http_Phase[i].Timeout;
|
||||
powermeter_phase["json_path"] = config.Powermeter_Http_Phase[i].JsonPath;
|
||||
}
|
||||
|
||||
JsonObject powerlimiter = doc.createNestedObject("powerlimiter");
|
||||
powerlimiter["enabled"] = config.PowerLimiter_Enabled;
|
||||
@ -146,6 +159,9 @@ bool ConfigurationClass::write()
|
||||
JsonObject battery = doc.createNestedObject("battery");
|
||||
battery["enabled"] = config.Battery_Enabled;
|
||||
|
||||
JsonObject huawei = doc.createNestedObject("huawei");
|
||||
huawei["enabled"] = config.Huawei_Enabled;
|
||||
|
||||
// Serialize JSON to file
|
||||
if (serializeJson(doc, f) == 0) {
|
||||
MessageOutput.println("Failed to write file");
|
||||
@ -164,7 +180,7 @@ bool ConfigurationClass::read()
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, f);
|
||||
if (error) {
|
||||
MessageOutput.printf("Failed to read file, using default configuration. Error: %s (capacity: %d)\r\n", error.c_str(), doc.capacity());
|
||||
MessageOutput.println("Failed to read file, using default configuration");
|
||||
}
|
||||
|
||||
JsonObject cfg = doc["cfg"];
|
||||
@ -297,7 +313,19 @@ bool ConfigurationClass::read()
|
||||
strlcpy(config.PowerMeter_MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter_MqttTopicPowerMeter3));
|
||||
config.PowerMeter_SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE;
|
||||
config.PowerMeter_SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
|
||||
config.PowerMeter_HttpIndividualRequests = powermeter["http_individual_requests"] | false;
|
||||
|
||||
JsonArray powermeter_http_phases = powermeter["http_phases"];
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
|
||||
|
||||
config.Powermeter_Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
|
||||
strlcpy(config.Powermeter_Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.Powermeter_Http_Phase[i].Url));
|
||||
strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderKey));
|
||||
strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.Powermeter_Http_Phase[i].HeaderValue));
|
||||
config.Powermeter_Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
|
||||
strlcpy(config.Powermeter_Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.Powermeter_Http_Phase[i].JsonPath));
|
||||
}
|
||||
|
||||
JsonObject powerlimiter = doc["powerlimiter"];
|
||||
config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED;
|
||||
@ -320,6 +348,9 @@ bool ConfigurationClass::read()
|
||||
JsonObject battery = doc["battery"];
|
||||
config.Battery_Enabled = battery["enabled"] | BATTERY_ENABLED;
|
||||
|
||||
JsonObject huawei = doc["huawei"];
|
||||
config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
@ -328,7 +359,7 @@ void ConfigurationClass::migrate()
|
||||
{
|
||||
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
|
||||
if (!f) {
|
||||
MessageOutput.println(F("Failed to open file, cancel migration"));
|
||||
MessageOutput.println("Failed to open file, cancel migration");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
130
src/HttpPowerMeter.cpp
Normal file
130
src/HttpPowerMeter.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "Configuration.h"
|
||||
#include "HttpPowerMeter.h"
|
||||
#include "MessageOutput.h"
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <FirebaseJson.h>
|
||||
|
||||
void HttpPowerMeterClass::init()
|
||||
{
|
||||
}
|
||||
|
||||
float HttpPowerMeterClass::getPower(int8_t phase)
|
||||
{
|
||||
return power[phase - 1];
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::updateValues()
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
char response[2000],
|
||||
errorMessage[256];
|
||||
bool success = true;
|
||||
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.Powermeter_Http_Phase[i];
|
||||
|
||||
if (!phaseConfig.Enabled || !success) {
|
||||
power[i] = 0.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0 || config.PowerMeter_HttpIndividualRequests) {
|
||||
if (!httpRequest(phaseConfig.Url, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
|
||||
response, sizeof(response), errorMessage, sizeof(errorMessage))) {
|
||||
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed. Error: %s\r\n",
|
||||
i + 1, errorMessage);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) {
|
||||
MessageOutput.printf("[HttpPowerMeter] Couldn't find a value with Json query \"%s\"\r\n", phaseConfig.JsonPath);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool HttpPowerMeterClass::httpRequest(const char* url, const char* httpHeader, const char* httpValue, uint32_t timeout,
|
||||
char* response, size_t responseSize, char* error, size_t errorSize)
|
||||
{
|
||||
WiFiClient* wifiClient = NULL;
|
||||
HTTPClient httpClient;
|
||||
|
||||
response[0] = '\0';
|
||||
error[0] = '\0';
|
||||
|
||||
if (String(url).substring(0, 6) == "https:") {
|
||||
wifiClient = new WiFiClientSecure;
|
||||
reinterpret_cast<WiFiClientSecure*>(wifiClient)->setInsecure();
|
||||
} else {
|
||||
wifiClient = new WiFiClient;
|
||||
}
|
||||
|
||||
if (!httpClient.begin(*wifiClient, url)) {
|
||||
snprintf_P(error, errorSize, "httpClient.begin failed");
|
||||
delete wifiClient;
|
||||
return false;
|
||||
}
|
||||
|
||||
httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
httpClient.setUserAgent("OpenDTU-OnBattery");
|
||||
httpClient.setConnectTimeout(timeout);
|
||||
httpClient.setTimeout(timeout);
|
||||
httpClient.addHeader("Content-Type", "application/json");
|
||||
httpClient.addHeader("Accept", "application/json");
|
||||
|
||||
if (strlen(httpHeader) > 0) {
|
||||
httpClient.addHeader(httpHeader, httpValue);
|
||||
}
|
||||
|
||||
int httpCode = httpClient.GET();
|
||||
|
||||
|
||||
if (httpCode == HTTP_CODE_OK) {
|
||||
String responseBody = httpClient.getString();
|
||||
|
||||
if (responseBody.length() > (responseSize - 1)) {
|
||||
snprintf_P(error, errorSize, "Response too large! Response length: %d Body start: %s",
|
||||
httpClient.getSize(), responseBody.c_str());
|
||||
} else {
|
||||
snprintf(response, responseSize, responseBody.c_str());
|
||||
}
|
||||
} else if (httpCode <= 0) {
|
||||
snprintf_P(error, errorSize, "Error: %s", httpClient.errorToString(httpCode).c_str());
|
||||
} else if (httpCode != HTTP_CODE_OK) {
|
||||
snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode);
|
||||
}
|
||||
|
||||
httpClient.end();
|
||||
delete wifiClient;
|
||||
|
||||
if (error[0] != '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float& value)
|
||||
{
|
||||
FirebaseJson firebaseJson;
|
||||
firebaseJson.setJsonData(jsonString);
|
||||
|
||||
FirebaseJsonData firebaseJsonResult;
|
||||
if (!firebaseJson.get(firebaseJsonResult, jsonPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = firebaseJsonResult.to<float>();
|
||||
|
||||
firebaseJson.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpPowerMeterClass HttpPowerMeter;
|
||||
205
src/Huawei_can.cpp
Normal file
205
src/Huawei_can.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2023 Malte Schmidt and others
|
||||
*/
|
||||
#include "Huawei_can.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "Configuration.h"
|
||||
#include <SPI.h>
|
||||
#include <mcp_can.h>
|
||||
|
||||
#include <math.h>
|
||||
|
||||
HuaweiCanClass HuaweiCan;
|
||||
|
||||
void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power)
|
||||
{
|
||||
initialized = false;
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
spi = new SPIClass(VSPI);
|
||||
spi->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs);
|
||||
pinMode(huawei_cs, OUTPUT);
|
||||
digitalWrite(huawei_cs, HIGH);
|
||||
|
||||
pinMode(huawei_irq, INPUT_PULLUP);
|
||||
_huawei_irq = huawei_irq;
|
||||
|
||||
CAN = new MCP_CAN(spi, huawei_cs);
|
||||
if (!CAN->begin(MCP_ANY, CAN_125KBPS, MCP_8MHZ) == CAN_OK) {
|
||||
MessageOutput.println("Error Initializing MCP2515...");
|
||||
return;
|
||||
}
|
||||
|
||||
MessageOutput.println("MCP2515 Initialized Successfully!");
|
||||
initialized = true;
|
||||
|
||||
// Change to normal mode to allow messages to be transmitted
|
||||
CAN->setMode(MCP_NORMAL);
|
||||
|
||||
pinMode(huawei_power, OUTPUT);
|
||||
digitalWrite(huawei_power, HIGH);
|
||||
_huawei_power = huawei_power;
|
||||
}
|
||||
|
||||
RectifierParameters_t * HuaweiCanClass::get()
|
||||
{
|
||||
return &_rp;
|
||||
}
|
||||
|
||||
uint32_t HuaweiCanClass::getLastUpdate()
|
||||
{
|
||||
return lastUpdate;
|
||||
}
|
||||
uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
// Requests current values from Huawei unit. Response is handled in onReceive
|
||||
void HuaweiCanClass::sendRequest()
|
||||
{
|
||||
if (previousMillis < millis()) {
|
||||
// Send extended message
|
||||
byte sndStat = CAN->sendMsgBuf(0x108040FE, 1, 8, data);
|
||||
if(sndStat == CAN_OK) {
|
||||
MessageOutput.println("Message Sent Successfully!");
|
||||
} else {
|
||||
MessageOutput.println("Error Sending Message...");
|
||||
}
|
||||
|
||||
previousMillis += 5000;
|
||||
}
|
||||
}
|
||||
|
||||
void HuaweiCanClass::onReceive(uint8_t* frame, uint8_t len)
|
||||
{
|
||||
if (len != 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t value = __bswap32(* reinterpret_cast<uint32_t*> (frame + 4));
|
||||
|
||||
switch (frame[1]) {
|
||||
case R48xx_DATA_INPUT_POWER:
|
||||
_rp.input_power = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_INPUT_FREQ:
|
||||
_rp.input_frequency = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_INPUT_CURRENT:
|
||||
_rp.input_current = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_POWER:
|
||||
_rp.output_power = value / 1024.0;
|
||||
// We'll only update last update on the important params
|
||||
lastUpdate = millis();
|
||||
break;
|
||||
|
||||
case R48xx_DATA_EFFICIENCY:
|
||||
_rp.efficiency = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_VOLTAGE:
|
||||
_rp.output_voltage = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_CURRENT_MAX:
|
||||
_rp.max_output_current = value / MAX_CURRENT_MULTIPLIER;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_INPUT_VOLTAGE:
|
||||
_rp.input_voltage = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_TEMPERATURE:
|
||||
_rp.output_temp = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_INPUT_TEMPERATURE:
|
||||
_rp.input_temp = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_CURRENT1:
|
||||
// printf("Output Current(1) %.02fA\r\n", value / 1024.0);
|
||||
// output_current = value / 1024.0;
|
||||
break;
|
||||
|
||||
case R48xx_DATA_OUTPUT_CURRENT:
|
||||
_rp.output_current = value / 1024.0;
|
||||
|
||||
/* This is normally the last parameter received. Print */
|
||||
lastUpdate = millis(); // We'll only update last update on the important params
|
||||
|
||||
MessageOutput.printf("In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power);
|
||||
MessageOutput.printf("Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power);
|
||||
MessageOutput.printf("Eff: %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// printf("Unknown parameter 0x%02X, 0x%04X\r\n",frame[1], value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HuaweiCanClass::loop()
|
||||
{
|
||||
|
||||
INT32U rxId;
|
||||
unsigned char len = 0;
|
||||
unsigned char rxBuf[8];
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei_Enabled || !initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!digitalRead(_huawei_irq)) {
|
||||
// If CAN_INT pin is low, read receive buffer
|
||||
CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s)
|
||||
|
||||
if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits)
|
||||
// MessageOutput.printf("Extended ID: 0x%.8lX DLC: %1d \n", (rxId & 0x1FFFFFFF), len);
|
||||
if ((rxId & 0x1FFFFFFF) == 0x1081407F) {
|
||||
onReceive(rxBuf, len);
|
||||
}
|
||||
// Other emitted codes not handled here are: 0x1081407E, 0x1081807E, 0x1081D27F, 0x1001117E, 0x100011FE, 0x108111FE, 0x108081FE. See:
|
||||
// https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c
|
||||
// https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/
|
||||
}
|
||||
}
|
||||
sendRequest();
|
||||
}
|
||||
|
||||
void HuaweiCanClass::setValue(float in, uint8_t parameterType)
|
||||
{
|
||||
uint16_t value;
|
||||
if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) {
|
||||
value = in * 1024;
|
||||
} else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) {
|
||||
value = in * MAX_CURRENT_MULTIPLIER;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t data[8] = {0x01, parameterType, 0x00, 0x00, 0x00, 0x00, (uint8_t)((value & 0xFF00) >> 8), (uint8_t)(value & 0xFF)};
|
||||
|
||||
// Send extended message
|
||||
byte sndStat = CAN->sendMsgBuf(0x108180FE, 1, 8, data);
|
||||
if (sndStat == CAN_OK) {
|
||||
MessageOutput.println("Message Sent Successfully!");
|
||||
} else {
|
||||
MessageOutput.println("Error Sending Message...");
|
||||
}
|
||||
}
|
||||
|
||||
void HuaweiCanClass::setPower(bool power) {
|
||||
digitalWrite(_huawei_power, !power);
|
||||
}
|
||||
@ -9,6 +9,10 @@
|
||||
#include "SunPosition.h"
|
||||
#include <Hoymiles.h>
|
||||
|
||||
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
|
||||
#define VSPI FSPI
|
||||
#endif
|
||||
|
||||
InverterSettingsClass InverterSettings;
|
||||
|
||||
void InverterSettingsClass::init()
|
||||
@ -17,27 +21,27 @@ void InverterSettingsClass::init()
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
|
||||
// Initialize inverter communication
|
||||
MessageOutput.print(F("Initialize Hoymiles interface... "));
|
||||
MessageOutput.print("Initialize Hoymiles interface... ");
|
||||
if (PinMapping.isValidNrf24Config()) {
|
||||
SPIClass* spiClass = new SPIClass(HSPI);
|
||||
SPIClass* spiClass = new SPIClass(VSPI);
|
||||
spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs);
|
||||
Hoymiles.setMessageOutput(&MessageOutput);
|
||||
Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq);
|
||||
|
||||
MessageOutput.println(F(" Setting radio PA level... "));
|
||||
MessageOutput.println(" Setting radio PA level... ");
|
||||
Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel);
|
||||
|
||||
MessageOutput.println(F(" Setting DTU serial... "));
|
||||
MessageOutput.println(" Setting DTU serial... ");
|
||||
Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial);
|
||||
|
||||
MessageOutput.println(F(" Setting poll interval... "));
|
||||
MessageOutput.println(" Setting poll interval... ");
|
||||
Hoymiles.setPollInterval(config.Dtu_PollInterval);
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial > 0) {
|
||||
MessageOutput.print(F(" Adding inverter: "));
|
||||
MessageOutput.print(" Adding inverter: ");
|
||||
MessageOutput.print(config.Inverter[i].Serial, HEX);
|
||||
MessageOutput.print(F(" - "));
|
||||
MessageOutput.print(" - ");
|
||||
MessageOutput.print(config.Inverter[i].Name);
|
||||
auto inv = Hoymiles.addInverter(
|
||||
config.Inverter[i].Name,
|
||||
@ -49,12 +53,12 @@ void InverterSettingsClass::init()
|
||||
inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast<ChannelNum_t>(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset);
|
||||
}
|
||||
}
|
||||
MessageOutput.println(F(" done"));
|
||||
MessageOutput.println(" done");
|
||||
}
|
||||
}
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
MessageOutput.println("Invalid pin config");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
110
src/Led_Single.cpp
Normal file
110
src/Led_Single.cpp
Normal file
@ -0,0 +1,110 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2023 Thomas Basler and others
|
||||
*/
|
||||
#include "Led_Single.h"
|
||||
#include "Configuration.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "PinMapping.h"
|
||||
#include <Hoymiles.h>
|
||||
|
||||
LedSingleClass LedSingle;
|
||||
|
||||
LedSingleClass::LedSingleClass()
|
||||
{
|
||||
}
|
||||
|
||||
void LedSingleClass::init()
|
||||
{
|
||||
_blinkTimeout.set(500);
|
||||
_updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL);
|
||||
|
||||
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
|
||||
auto& pin = PinMapping.get();
|
||||
|
||||
if (pin.led[i] >= 0) {
|
||||
pinMode(pin.led[i], OUTPUT);
|
||||
digitalWrite(pin.led[i], LOW);
|
||||
_ledActive++;
|
||||
}
|
||||
|
||||
_ledState[i] = LedState_t::Off;
|
||||
}
|
||||
}
|
||||
|
||||
void LedSingleClass::loop()
|
||||
{
|
||||
if (_ledActive == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_updateTimeout.occured()) {
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
// Update network status
|
||||
_ledState[0] = LedState_t::Off;
|
||||
|
||||
if (NetworkSettings.isConnected()) {
|
||||
_ledState[0] = LedState_t::Blink;
|
||||
}
|
||||
|
||||
struct tm timeinfo;
|
||||
if (getLocalTime(&timeinfo, 5) && (!config.Mqtt_Enabled || (config.Mqtt_Enabled && MqttSettings.getConnected()))) {
|
||||
_ledState[0] = LedState_t::On;
|
||||
}
|
||||
|
||||
// Update inverter status
|
||||
_ledState[1] = LedState_t::Off;
|
||||
if (Hoymiles.getNumInverters()) {
|
||||
bool allReachable = true;
|
||||
bool allProducing = true;
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
if (inv == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (inv->getEnablePolling()) {
|
||||
if (!inv->isReachable()) {
|
||||
allReachable = false;
|
||||
}
|
||||
if (!inv->isProducing()) {
|
||||
allProducing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// set LED status
|
||||
if (allReachable && allProducing) {
|
||||
_ledState[1] = LedState_t::On;
|
||||
}
|
||||
if (allReachable && !allProducing) {
|
||||
_ledState[1] = LedState_t::Blink;
|
||||
}
|
||||
}
|
||||
|
||||
_updateTimeout.reset();
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
|
||||
auto& pin = PinMapping.get();
|
||||
|
||||
if (pin.led[i] < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (_ledState[i]) {
|
||||
case LedState_t::Off:
|
||||
digitalWrite(pin.led[i], LOW);
|
||||
break;
|
||||
case LedState_t::On:
|
||||
digitalWrite(pin.led[i], HIGH);
|
||||
break;
|
||||
case LedState_t::Blink:
|
||||
if (_blinkTimeout.occured()) {
|
||||
digitalWrite(pin.led[i], !digitalRead(pin.led[i]));
|
||||
_blinkTimeout.reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,26 +121,26 @@ void MqttHandleHassClass::publishField(std::shared_ptr<InverterAbstract> inv, Ch
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = name;
|
||||
root[F("stat_t")] = stateTopic;
|
||||
root[F("uniq_id")] = serial + "_ch" + chanNum + "_" + fieldName;
|
||||
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[F("unit_of_meas")] = unit_of_measure;
|
||||
root["unit_of_meas"] = unit_of_measure;
|
||||
}
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj, inv);
|
||||
|
||||
if (Configuration.get().Mqtt_Hass_Expire) {
|
||||
root[F("exp_aft")] = Hoymiles.getNumInverters() * Configuration.get().Mqtt_PublishInterval * 3;
|
||||
root["exp_aft"] = Hoymiles.getNumInverters() * Configuration.get().Mqtt_PublishInterval * 3;
|
||||
}
|
||||
if (devCls != 0) {
|
||||
root[F("dev_cla")] = devCls;
|
||||
root["dev_cla"] = devCls;
|
||||
}
|
||||
if (stateCls != 0) {
|
||||
root[F("stat_cla")] = stateCls;
|
||||
root["stat_cla"] = stateCls;
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
@ -166,17 +166,17 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
|
||||
String cmdTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + buttonId;
|
||||
root["name"] = String(inv->name()) + " " + caption;
|
||||
root["uniq_id"] = serial + "_" + buttonId;
|
||||
if (strcmp(icon, "")) {
|
||||
root[F("ic")] = icon;
|
||||
root["ic"] = icon;
|
||||
}
|
||||
if (strcmp(deviceClass, "")) {
|
||||
root[F("dev_cla")] = deviceClass;
|
||||
root["dev_cla"] = deviceClass;
|
||||
}
|
||||
root[F("ent_cat")] = category;
|
||||
root[F("cmd_t")] = cmdTopic;
|
||||
root[F("payload_press")] = payload;
|
||||
root["ent_cat"] = category;
|
||||
root["cmd_t"] = cmdTopic;
|
||||
root["payload_press"] = payload;
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj, inv);
|
||||
@ -205,17 +205,17 @@ void MqttHandleHassClass::publishInverterNumber(
|
||||
String statTopic = MqttSettings.getPrefix() + serial + "/" + stateTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + buttonId;
|
||||
root["name"] = String(inv->name()) + " " + caption;
|
||||
root["uniq_id"] = serial + "_" + buttonId;
|
||||
if (strcmp(icon, "")) {
|
||||
root[F("ic")] = icon;
|
||||
root["ic"] = icon;
|
||||
}
|
||||
root[F("ent_cat")] = category;
|
||||
root[F("cmd_t")] = cmdTopic;
|
||||
root[F("stat_t")] = statTopic;
|
||||
root[F("unit_of_meas")] = unitOfMeasure;
|
||||
root[F("min")] = min;
|
||||
root[F("max")] = max;
|
||||
root["ent_cat"] = category;
|
||||
root["cmd_t"] = cmdTopic;
|
||||
root["stat_t"] = statTopic;
|
||||
root["unit_of_meas"] = unitOfMeasure;
|
||||
root["min"] = min;
|
||||
root["max"] = max;
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj, inv);
|
||||
@ -240,11 +240,11 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
|
||||
String statTopic = MqttSettings.getPrefix() + serial + "/" + subTopic;
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
root[F("name")] = String(inv->name()) + " " + caption;
|
||||
root[F("uniq_id")] = serial + "_" + sensorId;
|
||||
root[F("stat_t")] = statTopic;
|
||||
root[F("pl_on")] = payload_on;
|
||||
root[F("pl_off")] = payload_off;
|
||||
root["name"] = String(inv->name()) + " " + caption;
|
||||
root["uniq_id"] = serial + "_" + sensorId;
|
||||
root["stat_t"] = statTopic;
|
||||
root["pl_on"] = payload_on;
|
||||
root["pl_off"] = payload_off;
|
||||
|
||||
JsonObject deviceObj = root.createNestedObject("dev");
|
||||
createDeviceInfo(deviceObj, inv);
|
||||
@ -256,12 +256,12 @@ void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAb
|
||||
|
||||
void MqttHandleHassClass::createDeviceInfo(JsonObject& object, std::shared_ptr<InverterAbstract> inv)
|
||||
{
|
||||
object[F("name")] = inv->name();
|
||||
object[F("ids")] = inv->serialString();
|
||||
object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString();
|
||||
object[F("mf")] = F("OpenDTU");
|
||||
object[F("mdl")] = inv->typeName();
|
||||
object[F("sw")] = AUTO_GIT_HASH;
|
||||
object["name"] = inv->name();
|
||||
object["ids"] = inv->serialString();
|
||||
object["cu"] = String("http://") + NetworkSettings.localIP().toString();
|
||||
object["mf"] = "OpenDTU";
|
||||
object["mdl"] = inv->typeName();
|
||||
object["sw"] = AUTO_GIT_HASH;
|
||||
}
|
||||
|
||||
void MqttHandleHassClass::publish(const String& subtopic, const String& payload)
|
||||
|
||||
115
src/MqttHandleHuawei.cpp
Normal file
115
src/MqttHandleHuawei.cpp
Normal file
@ -0,0 +1,115 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "Huawei_can.h"
|
||||
// #include "Failsafe.h"
|
||||
#include "WebApi_Huawei.h"
|
||||
#include <ctime>
|
||||
|
||||
#define TOPIC_SUB_LIMIT_ONLINE_VOLTAGE "limit_online_voltage"
|
||||
#define TOPIC_SUB_LIMIT_ONLINE_CURRENT "limit_online_current"
|
||||
#define TOPIC_SUB_POWER "power"
|
||||
|
||||
MqttHandleHuaweiClass MqttHandleHuawei;
|
||||
|
||||
void MqttHandleHuaweiClass::init()
|
||||
{
|
||||
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 topic = MqttSettings.getPrefix();
|
||||
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_ONLINE_VOLTAGE).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_LIMIT_ONLINE_CURRENT).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
MqttSettings.subscribe(String(topic + "huawei/cmd/" + TOPIC_SUB_POWER).c_str(), 0, std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
_lastPublish = millis();
|
||||
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::loop()
|
||||
{
|
||||
if (!MqttSettings.getConnected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
if (!config.Huawei_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RectifierParameters_t *rp = HuaweiCan.get();
|
||||
|
||||
if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) {
|
||||
MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000));
|
||||
MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage));
|
||||
MqttSettings.publish("huawei/input_current", String(rp->input_current));
|
||||
MqttSettings.publish("huawei/input_power", String(rp->input_power));
|
||||
MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage));
|
||||
MqttSettings.publish("huawei/output_current", String(rp->output_current));
|
||||
MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current));
|
||||
MqttSettings.publish("huawei/output_power", String(rp->output_power));
|
||||
MqttSettings.publish("huawei/input_temp", String(rp->input_temp));
|
||||
MqttSettings.publish("huawei/output_temp", String(rp->output_temp));
|
||||
MqttSettings.publish("huawei/efficiency", String(rp->efficiency));
|
||||
|
||||
|
||||
yield();
|
||||
_lastPublish = millis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MqttHandleHuaweiClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics
|
||||
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
|
||||
|
||||
char* setting;
|
||||
char* rest = &token_topic[strlen(config.Mqtt_Topic)];
|
||||
|
||||
strtok_r(rest, "/", &rest); // Remove "huawei"
|
||||
strtok_r(rest, "/", &rest); // Remove "cmd"
|
||||
|
||||
setting = strtok_r(rest, "/", &rest);
|
||||
|
||||
if (setting == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
char* strlimit = new char[len + 1];
|
||||
memcpy(strlimit, payload, len);
|
||||
strlimit[len] = '\0';
|
||||
float payload_val = strtof(strlimit, NULL);
|
||||
delete[] strlimit;
|
||||
|
||||
if (!strcmp(setting, TOPIC_SUB_LIMIT_ONLINE_VOLTAGE)) {
|
||||
// Set voltage limit
|
||||
MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val);
|
||||
HuaweiCan.setValue(payload_val, HUAWEI_ONLINE_VOLTAGE);
|
||||
|
||||
} else if (!strcmp(setting, TOPIC_SUB_LIMIT_ONLINE_CURRENT)) {
|
||||
// Set current limit
|
||||
MessageOutput.printf("Limit Current: %f A\r\n", payload_val);
|
||||
HuaweiCan.setValue(payload_val, HUAWEI_ONLINE_CURRENT);
|
||||
} else if (!strcmp(setting, TOPIC_SUB_POWER)) {
|
||||
// Control power on/off
|
||||
MessageOutput.printf("Power: %f A\r\n", payload_val);
|
||||
if(payload_val > 0) {
|
||||
HuaweiCan.setPower(true);
|
||||
} else {
|
||||
HuaweiCan.setPower(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,7 +185,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
|
||||
if (inv == nullptr) {
|
||||
MessageOutput.println(F("Inverter not found"));
|
||||
MessageOutput.println("Inverter not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -14,11 +14,11 @@ void MqttSettingsClass::NetworkEvent(network_event event)
|
||||
{
|
||||
switch (event) {
|
||||
case network_event::NETWORK_GOT_IP:
|
||||
MessageOutput.println(F("Network connected"));
|
||||
MessageOutput.println("Network connected");
|
||||
performConnect();
|
||||
break;
|
||||
case network_event::NETWORK_DISCONNECTED:
|
||||
MessageOutput.println(F("Network lost connection"));
|
||||
MessageOutput.println("Network lost connection");
|
||||
mqttReconnectTimer.detach(); // ensure we don't reconnect to MQTT while reconnecting to Wi-Fi
|
||||
break;
|
||||
default:
|
||||
@ -28,7 +28,7 @@ void MqttSettingsClass::NetworkEvent(network_event event)
|
||||
|
||||
void MqttSettingsClass::onMqttConnect(bool sessionPresent)
|
||||
{
|
||||
MessageOutput.println(F("Connected to MQTT."));
|
||||
MessageOutput.println("Connected to MQTT.");
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
publish(config.Mqtt_LwtTopic, config.Mqtt_LwtValue_Online);
|
||||
|
||||
@ -51,30 +51,30 @@ void MqttSettingsClass::unsubscribe(const String& topic)
|
||||
|
||||
void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason)
|
||||
{
|
||||
MessageOutput.println(F("Disconnected from MQTT."));
|
||||
MessageOutput.println("Disconnected from MQTT.");
|
||||
|
||||
MessageOutput.print(F("Disconnect reason:"));
|
||||
MessageOutput.print("Disconnect reason:");
|
||||
switch (reason) {
|
||||
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
|
||||
MessageOutput.println(F("TCP_DISCONNECTED"));
|
||||
MessageOutput.println("TCP_DISCONNECTED");
|
||||
break;
|
||||
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||
MessageOutput.println(F("MQTT_UNACCEPTABLE_PROTOCOL_VERSION"));
|
||||
MessageOutput.println("MQTT_UNACCEPTABLE_PROTOCOL_VERSION");
|
||||
break;
|
||||
case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
|
||||
MessageOutput.println(F("MQTT_IDENTIFIER_REJECTED"));
|
||||
MessageOutput.println("MQTT_IDENTIFIER_REJECTED");
|
||||
break;
|
||||
case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
|
||||
MessageOutput.println(F("MQTT_SERVER_UNAVAILABLE"));
|
||||
MessageOutput.println("MQTT_SERVER_UNAVAILABLE");
|
||||
break;
|
||||
case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
|
||||
MessageOutput.println(F("MQTT_MALFORMED_CREDENTIALS"));
|
||||
MessageOutput.println("MQTT_MALFORMED_CREDENTIALS");
|
||||
break;
|
||||
case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED:
|
||||
MessageOutput.println(F("MQTT_NOT_AUTHORIZED"));
|
||||
MessageOutput.println("MQTT_NOT_AUTHORIZED");
|
||||
break;
|
||||
default:
|
||||
MessageOutput.println(F("Unknown"));
|
||||
MessageOutput.println("Unknown");
|
||||
}
|
||||
mqttReconnectTimer.once(
|
||||
2, +[](MqttSettingsClass* instance) { instance->performConnect(); }, this);
|
||||
@ -82,7 +82,7 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
|
||||
|
||||
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
MessageOutput.print(F("Received MQTT message on topic: "));
|
||||
MessageOutput.print("Received MQTT message on topic: ");
|
||||
MessageOutput.println(topic);
|
||||
|
||||
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
|
||||
@ -97,7 +97,7 @@ void MqttSettingsClass::performConnect()
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
MessageOutput.println(F("Connecting to MQTT..."));
|
||||
MessageOutput.println("Connecting to MQTT...");
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
willTopic = getPrefix() + config.Mqtt_LwtTopic;
|
||||
clientId = NetworkSettings.getApName();
|
||||
|
||||
@ -29,19 +29,19 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
|
||||
{
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_ETH_START:
|
||||
MessageOutput.println(F("ETH start"));
|
||||
MessageOutput.println("ETH start");
|
||||
if (_networkMode == network_mode::Ethernet) {
|
||||
raiseEvent(network_event::NETWORK_START);
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_STOP:
|
||||
MessageOutput.println(F("ETH stop"));
|
||||
MessageOutput.println("ETH stop");
|
||||
if (_networkMode == network_mode::Ethernet) {
|
||||
raiseEvent(network_event::NETWORK_STOP);
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_CONNECTED:
|
||||
MessageOutput.println(F("ETH connected"));
|
||||
MessageOutput.println("ETH connected");
|
||||
_ethConnected = true;
|
||||
raiseEvent(network_event::NETWORK_CONNECTED);
|
||||
break;
|
||||
@ -52,22 +52,22 @@ void NetworkSettingsClass::NetworkEvent(WiFiEvent_t event)
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||
MessageOutput.println(F("ETH disconnected"));
|
||||
MessageOutput.println("ETH disconnected");
|
||||
_ethConnected = false;
|
||||
if (_networkMode == network_mode::Ethernet) {
|
||||
raiseEvent(network_event::NETWORK_DISCONNECTED);
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
MessageOutput.println(F("WiFi connected"));
|
||||
MessageOutput.println("WiFi connected");
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
raiseEvent(network_event::NETWORK_CONNECTED);
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
MessageOutput.println(F("WiFi disconnected"));
|
||||
MessageOutput.println("WiFi disconnected");
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
MessageOutput.println(F("Try reconnecting"));
|
||||
MessageOutput.println("Try reconnecting");
|
||||
WiFi.reconnect();
|
||||
raiseEvent(network_event::NETWORK_DISCONNECTED);
|
||||
}
|
||||
@ -154,7 +154,7 @@ void NetworkSettingsClass::loop()
|
||||
if (_ethConnected) {
|
||||
if (_networkMode != network_mode::Ethernet) {
|
||||
// Do stuff when switching to Ethernet mode
|
||||
MessageOutput.println(F("Switch to Ethernet mode"));
|
||||
MessageOutput.println("Switch to Ethernet mode");
|
||||
_networkMode = network_mode::Ethernet;
|
||||
WiFi.mode(WIFI_MODE_NULL);
|
||||
setStaticIp();
|
||||
@ -163,7 +163,7 @@ void NetworkSettingsClass::loop()
|
||||
} else
|
||||
if (_networkMode != network_mode::WiFi) {
|
||||
// Do stuff when switching to Ethernet mode
|
||||
MessageOutput.println(F("Switch to WiFi mode"));
|
||||
MessageOutput.println("Switch to WiFi mode");
|
||||
_networkMode = network_mode::WiFi;
|
||||
enableAdminMode();
|
||||
applyConfig();
|
||||
@ -184,7 +184,7 @@ void NetworkSettingsClass::loop()
|
||||
// seconds, disable the internal Access Point
|
||||
if (adminTimeoutCounter > ADMIN_TIMEOUT) {
|
||||
adminEnabled = false;
|
||||
MessageOutput.println(F("Admin mode disabled"));
|
||||
MessageOutput.println("Admin mode disabled");
|
||||
setupMode();
|
||||
}
|
||||
// It's nearly not possible to use the internal AP if the
|
||||
@ -195,16 +195,16 @@ void NetworkSettingsClass::loop()
|
||||
connectRedoTimer = 0;
|
||||
} else {
|
||||
if (connectTimeoutTimer > WIFI_RECONNECT_TIMEOUT && !forceDisconnection) {
|
||||
MessageOutput.print(F("Disable search for AP... "));
|
||||
MessageOutput.print("Disable search for AP... ");
|
||||
WiFi.mode(WIFI_AP);
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
connectRedoTimer = 0;
|
||||
forceDisconnection = true;
|
||||
}
|
||||
if (connectRedoTimer > WIFI_RECONNECT_REDO_TIMEOUT && forceDisconnection) {
|
||||
MessageOutput.print(F("Enable search for AP... "));
|
||||
MessageOutput.print("Enable search for AP... ");
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
applyConfig();
|
||||
connectTimeoutTimer = 0;
|
||||
forceDisconnection = false;
|
||||
@ -222,28 +222,28 @@ void NetworkSettingsClass::applyConfig()
|
||||
if (!strcmp(Configuration.get().WiFi_Ssid, "")) {
|
||||
return;
|
||||
}
|
||||
MessageOutput.print(F("Configuring WiFi STA using "));
|
||||
MessageOutput.print("Configuring WiFi STA using ");
|
||||
if (strcmp(WiFi.SSID().c_str(), Configuration.get().WiFi_Ssid) || strcmp(WiFi.psk().c_str(), Configuration.get().WiFi_Password)) {
|
||||
MessageOutput.print(F("new credentials... "));
|
||||
MessageOutput.print("new credentials... ");
|
||||
WiFi.begin(
|
||||
Configuration.get().WiFi_Ssid,
|
||||
Configuration.get().WiFi_Password);
|
||||
} else {
|
||||
MessageOutput.print(F("existing credentials... "));
|
||||
MessageOutput.print("existing credentials... ");
|
||||
WiFi.begin();
|
||||
}
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
setStaticIp();
|
||||
}
|
||||
|
||||
void NetworkSettingsClass::setHostname()
|
||||
{
|
||||
MessageOutput.print(F("Setting Hostname... "));
|
||||
MessageOutput.print("Setting Hostname... ");
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
if (WiFi.hostname(getHostname())) {
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println(F("failed"));
|
||||
MessageOutput.println("failed");
|
||||
}
|
||||
|
||||
// Evil bad hack to get the hostname set up correctly
|
||||
@ -253,9 +253,9 @@ void NetworkSettingsClass::setHostname()
|
||||
}
|
||||
else if (_networkMode == network_mode::Ethernet) {
|
||||
if (ETH.setHostname(getHostname().c_str())) {
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.println(F("failed"));
|
||||
MessageOutput.println("failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -264,34 +264,34 @@ void NetworkSettingsClass::setStaticIp()
|
||||
{
|
||||
if (_networkMode == network_mode::WiFi) {
|
||||
if (Configuration.get().WiFi_Dhcp) {
|
||||
MessageOutput.print(F("Configuring WiFi STA DHCP IP... "));
|
||||
MessageOutput.print("Configuring WiFi STA DHCP IP... ");
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.print(F("Configuring WiFi STA static IP... "));
|
||||
MessageOutput.print("Configuring WiFi STA static IP... ");
|
||||
WiFi.config(
|
||||
IPAddress(Configuration.get().WiFi_Ip),
|
||||
IPAddress(Configuration.get().WiFi_Gateway),
|
||||
IPAddress(Configuration.get().WiFi_Netmask),
|
||||
IPAddress(Configuration.get().WiFi_Dns1),
|
||||
IPAddress(Configuration.get().WiFi_Dns2));
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
}
|
||||
}
|
||||
else if (_networkMode == network_mode::Ethernet) {
|
||||
if (Configuration.get().WiFi_Dhcp) {
|
||||
MessageOutput.print(F("Configuring Ethernet DHCP IP... "));
|
||||
MessageOutput.print("Configuring Ethernet DHCP IP... ");
|
||||
ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
} else {
|
||||
MessageOutput.print(F("Configuring Ethernet static IP... "));
|
||||
MessageOutput.print("Configuring Ethernet static IP... ");
|
||||
ETH.config(
|
||||
IPAddress(Configuration.get().WiFi_Ip),
|
||||
IPAddress(Configuration.get().WiFi_Gateway),
|
||||
IPAddress(Configuration.get().WiFi_Netmask),
|
||||
IPAddress(Configuration.get().WiFi_Dns1),
|
||||
IPAddress(Configuration.get().WiFi_Dns2));
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,14 @@
|
||||
#define DISPLAY_RESET 255
|
||||
#endif
|
||||
|
||||
#ifndef LED0
|
||||
#define LED0 -1
|
||||
#endif
|
||||
|
||||
#ifndef LED1
|
||||
#define LED1 -1
|
||||
#endif
|
||||
|
||||
PinMappingClass PinMapping;
|
||||
|
||||
PinMappingClass::PinMappingClass()
|
||||
@ -66,6 +74,15 @@ PinMappingClass::PinMappingClass()
|
||||
|
||||
_pinMapping.battery_rx = PYLONTECH_PIN_RX;
|
||||
_pinMapping.battery_tx = PYLONTECH_PIN_TX;
|
||||
|
||||
_pinMapping.huawei_miso = HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = HUAWEI_PIN_MOSI;
|
||||
_pinMapping.huawei_clk = HUAWEI_PIN_SCLK;
|
||||
_pinMapping.huawei_cs = HUAWEI_PIN_CS;
|
||||
_pinMapping.huawei_irq = HUAWEI_PIN_IRQ;
|
||||
_pinMapping.huawei_power = HUAWEI_PIN_POWER;
|
||||
_pinMapping.led[0] = LED0;
|
||||
_pinMapping.led[1] = LED1;
|
||||
}
|
||||
|
||||
PinMapping_t& PinMappingClass::get()
|
||||
@ -85,7 +102,7 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
// Deserialize the JSON document
|
||||
DeserializationError error = deserializeJson(doc, f);
|
||||
if (error) {
|
||||
MessageOutput.println(F("Failed to read file, using default configuration"));
|
||||
MessageOutput.println("Failed to read file, using default configuration");
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < doc.size(); i++) {
|
||||
@ -124,6 +141,16 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | PYLONTECH_PIN_RX;
|
||||
_pinMapping.battery_tx = doc[i]["battery"]["tx"] | PYLONTECH_PIN_TX;
|
||||
|
||||
_pinMapping.huawei_miso = doc[i]["huawei"]["miso"] | HUAWEI_PIN_MISO;
|
||||
_pinMapping.huawei_mosi = doc[i]["huawei"]["mosi"] | HUAWEI_PIN_MOSI;
|
||||
_pinMapping.huawei_clk = doc[i]["huawei"]["clk"] | HUAWEI_PIN_SCLK;
|
||||
_pinMapping.huawei_irq = doc[i]["huawei"]["irq"] | HUAWEI_PIN_IRQ;
|
||||
_pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS;
|
||||
_pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER;
|
||||
|
||||
_pinMapping.led[0] = doc[i]["led"]["led0"] | LED0;
|
||||
_pinMapping.led[1] = doc[i]["led"]["led1"] | LED1;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -133,12 +160,12 @@ bool PinMappingClass::init(const String& deviceMapping)
|
||||
|
||||
bool PinMappingClass::isValidNrf24Config()
|
||||
{
|
||||
return _pinMapping.nrf24_clk > 0
|
||||
&& _pinMapping.nrf24_cs > 0
|
||||
&& _pinMapping.nrf24_en > 0
|
||||
&& _pinMapping.nrf24_irq > 0
|
||||
&& _pinMapping.nrf24_miso > 0
|
||||
&& _pinMapping.nrf24_mosi > 0;
|
||||
return _pinMapping.nrf24_clk >= 0
|
||||
&& _pinMapping.nrf24_cs >= 0
|
||||
&& _pinMapping.nrf24_en >= 0
|
||||
&& _pinMapping.nrf24_irq >= 0
|
||||
&& _pinMapping.nrf24_miso >= 0
|
||||
&& _pinMapping.nrf24_mosi >= 0;
|
||||
}
|
||||
|
||||
bool PinMappingClass::isValidEthConfig()
|
||||
@ -157,3 +184,13 @@ bool PinMappingClass::isValidBatteryConfig()
|
||||
return _pinMapping.battery_rx > 0
|
||||
&& _pinMapping.battery_tx > 0;
|
||||
}
|
||||
|
||||
bool PinMappingClass::isValidHuaweiConfig()
|
||||
{
|
||||
return _pinMapping.huawei_miso > 0
|
||||
&& _pinMapping.huawei_mosi > 0
|
||||
&& _pinMapping.huawei_clk > 0
|
||||
&& _pinMapping.huawei_irq > 0
|
||||
&& _pinMapping.huawei_cs > 0
|
||||
&& _pinMapping.huawei_power > 0;
|
||||
}
|
||||
|
||||
@ -17,9 +17,6 @@ PowerLimiterClass PowerLimiter;
|
||||
|
||||
void PowerLimiterClass::init()
|
||||
{
|
||||
_lastCommandSent = 0;
|
||||
_lastLoop = 0;
|
||||
_lastRequestedPowerLimit = 0;
|
||||
}
|
||||
|
||||
void PowerLimiterClass::loop()
|
||||
@ -44,10 +41,16 @@ void PowerLimiterClass::loop()
|
||||
}
|
||||
|
||||
float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC);
|
||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||
float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
|
||||
|
||||
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000) {
|
||||
// If the last inverter update is too old, don't do anything.
|
||||
// If the last inverter update was before the last limit updated, don't do anything.
|
||||
// Also give the Power meter 3 seconds time to recognize power changes because of the last set limit
|
||||
// and also because the Hoymiles MPPT might not react immediately.
|
||||
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000
|
||||
|| inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime
|
||||
|| PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -56,7 +59,6 @@ void PowerLimiterClass::loop()
|
||||
dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing());
|
||||
}
|
||||
|
||||
|
||||
while(true) {
|
||||
switch(_plState) {
|
||||
case STATE_DISCOVER:
|
||||
@ -103,7 +105,7 @@ void PowerLimiterClass::loop()
|
||||
}
|
||||
|
||||
if (!canUseDirectSolarPower()) {
|
||||
if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGTH)
|
||||
if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)
|
||||
_plState = STATE_NORMAL_OPERATION;
|
||||
else
|
||||
_plState = STATE_OFF;
|
||||
@ -120,14 +122,16 @@ void PowerLimiterClass::loop()
|
||||
_plState = STATE_OFF;
|
||||
break;
|
||||
}
|
||||
if (canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGTH)) {
|
||||
if (canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)) {
|
||||
_plState = STATE_CONSUME_SOLAR_POWER_ONLY;
|
||||
break;
|
||||
}
|
||||
|
||||
// check if grid power consumption is not within the upper and lower threshold of the target consumption
|
||||
if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
||||
newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) {
|
||||
newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
||||
_lastRequestedPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
||||
_lastRequestedPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) ) {
|
||||
return;
|
||||
}
|
||||
setNewPowerLimit(inverter, newPowerLimit);;
|
||||
@ -175,14 +179,16 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
|
||||
|
||||
MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n",
|
||||
victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit);
|
||||
|
||||
if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) {
|
||||
|
||||
// Safety check: Are the power meter values not too old?
|
||||
// Are the reported inverter data not too old?
|
||||
if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)
|
||||
&& millis() - inverter->Statistics()->getLastUpdate() < (15 * 1000)) {
|
||||
if (config.PowerLimiter_IsInverterBehindPowerMeter) {
|
||||
// If the inverter the behind the power meter (part of measurement),
|
||||
// the produced power of this inverter has also to be taken into account.
|
||||
// We don't use FLD_PAC from the statistics, because that
|
||||
// data might be too old and unrelieable.
|
||||
newPowerLimit += _lastRequestedPowerLimit;
|
||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||
newPowerLimit += static_cast<int>(acPower);
|
||||
}
|
||||
|
||||
newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
|
||||
@ -225,6 +231,8 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver
|
||||
MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);
|
||||
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
|
||||
_lastRequestedPowerLimit = newPowerLimit;
|
||||
// wait for the next inverter update (+ 3 seconds to make sure the limit got applied)
|
||||
_lastLimitSetTime = millis();
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +249,7 @@ float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr<InverterAbstrac
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||
float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC);
|
||||
|
||||
if (dcVoltage <= 0.0) {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
#include "PowerMeter.h"
|
||||
#include "Configuration.h"
|
||||
#include "HttpPowerMeter.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "SDM.h"
|
||||
@ -23,54 +24,70 @@ void PowerMeterClass::init()
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_lastPowerMeterCheck = 0;
|
||||
_lastPowerMeterUpdate = 0;
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter2, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter3, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
|
||||
if (config.PowerMeter_Enabled && config.PowerMeter_Source == 0) {
|
||||
if (strlen(config.PowerMeter_MqttTopicPowerMeter1) > 0) {
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter1, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
if (strlen(config.PowerMeter_MqttTopicPowerMeter2) > 0) {
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter2, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
if (strlen(config.PowerMeter_MqttTopicPowerMeter3) > 0) {
|
||||
MqttSettings.subscribe(config.PowerMeter_MqttTopicPowerMeter3, 0, std::bind(&PowerMeterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
}
|
||||
|
||||
mqttInitDone = true;
|
||||
|
||||
sdm.begin();
|
||||
HttpPowerMeter.init();
|
||||
}
|
||||
|
||||
void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
if(config.PowerMeter_Enabled && config.PowerMeter_Source == 0){
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter1) == 0) {
|
||||
_powerMeter1Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter2) == 0) {
|
||||
_powerMeter2Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter3) == 0) {
|
||||
_powerMeter3Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal());
|
||||
if (config.PowerMeter_Enabled && config.PowerMeter_Source != SOURCE_MQTT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter1) == 0) {
|
||||
_powerMeter1Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter2) == 0) {
|
||||
_powerMeter2Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
if (strcmp(topic, config.PowerMeter_MqttTopicPowerMeter3) == 0) {
|
||||
_powerMeter3Power = std::stof(std::string(reinterpret_cast<const char*>(payload), (unsigned int)len));
|
||||
}
|
||||
|
||||
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal());
|
||||
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
|
||||
float PowerMeterClass::getPowerTotal(){
|
||||
float PowerMeterClass::getPowerTotal()
|
||||
{
|
||||
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
|
||||
}
|
||||
|
||||
uint32_t PowerMeterClass::getLastPowerMeterUpdate(){
|
||||
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
|
||||
{
|
||||
return _lastPowerMeterUpdate;
|
||||
}
|
||||
|
||||
void PowerMeterClass::mqtt(){
|
||||
if (!MqttSettings.getConnected()){
|
||||
void PowerMeterClass::mqtt()
|
||||
{
|
||||
if (!MqttSettings.getConnected()) {
|
||||
return;
|
||||
}else{
|
||||
} else {
|
||||
String topic = "powermeter";
|
||||
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power));
|
||||
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power));
|
||||
@ -88,33 +105,47 @@ void PowerMeterClass::loop()
|
||||
{
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
if(config.PowerMeter_Enabled && millis() - _lastPowerMeterUpdate >= (config.PowerMeter_Interval * 1000)){
|
||||
uint8_t _address = config.PowerMeter_SdmAddress;
|
||||
if(config.PowerMeter_Source == 1){
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = 0.0;
|
||||
_powerMeter3Power = 0.0;
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = 0.0;
|
||||
_powerMeter3Voltage = 0.0;
|
||||
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
}
|
||||
if(config.PowerMeter_Source == 2){
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
|
||||
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
|
||||
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
|
||||
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
}
|
||||
|
||||
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\n", getPowerTotal());
|
||||
|
||||
mqtt();
|
||||
if (!config.PowerMeter_Enabled
|
||||
|| (millis() - _lastPowerMeterCheck) < (config.PowerMeter_Interval * 1000)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t _address = config.PowerMeter_SdmAddress;
|
||||
|
||||
if (config.PowerMeter_Source== SOURCE_SDM1PH) {
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = 0.0;
|
||||
_powerMeter3Power = 0.0;
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = 0.0;
|
||||
_powerMeter3Voltage = 0.0;
|
||||
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
else if (config.PowerMeter_Source == SOURCE_SDM3PH) {
|
||||
_powerMeter1Power = static_cast<float>(sdm.readVal(SDM_PHASE_1_POWER, _address));
|
||||
_powerMeter2Power = static_cast<float>(sdm.readVal(SDM_PHASE_2_POWER, _address));
|
||||
_powerMeter3Power = static_cast<float>(sdm.readVal(SDM_PHASE_3_POWER, _address));
|
||||
_powerMeter1Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address));
|
||||
_powerMeter2Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address));
|
||||
_powerMeter3Voltage = static_cast<float>(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address));
|
||||
_PowerMeterImport = static_cast<float>(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address));
|
||||
_PowerMeterExport = static_cast<float>(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address));
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
else if (config.PowerMeter_Source == SOURCE_HTTP) {
|
||||
if (HttpPowerMeter.updateValues()) {
|
||||
_powerMeter1Power = HttpPowerMeter.getPower(1);
|
||||
_powerMeter2Power = HttpPowerMeter.getPower(2);
|
||||
_powerMeter3Power = HttpPowerMeter.getPower(3);
|
||||
_lastPowerMeterUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal());
|
||||
|
||||
mqtt();
|
||||
|
||||
_lastPowerMeterCheck = millis();
|
||||
}
|
||||
|
||||
@ -133,6 +133,7 @@ void PylontechCanReceiverClass::parseCanPackets()
|
||||
Battery.stateOfCharge = this->readUnsignedInt16(rx_message.data);
|
||||
Battery.stateOfChargeLastUpdate = millis();
|
||||
Battery.stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);
|
||||
Battery.lastUpdate = millis();
|
||||
|
||||
#ifdef PYLONTECH_DEBUG_ENABLED
|
||||
MessageOutput.printf("[Pylontech] soc: %d soh: %d\n",
|
||||
|
||||
@ -41,6 +41,10 @@ void WebApiClass::init()
|
||||
_webApiWsLive.init(&_server);
|
||||
_webApiWsVedirectLive.init(&_server);
|
||||
_webApiVedirect.init(&_server);
|
||||
_webApiWsHuaweiLive.init(&_server);
|
||||
_webApiHuaweiClass.init(&_server);
|
||||
_webApiWsPylontechLive.init(&_server);
|
||||
_webApiPylontechClass.init(&_server);
|
||||
|
||||
_server.begin();
|
||||
}
|
||||
@ -70,6 +74,10 @@ void WebApiClass::loop()
|
||||
_webApiWsLive.loop();
|
||||
_webApiWsVedirectLive.loop();
|
||||
_webApiVedirect.loop();
|
||||
_webApiWsHuaweiLive.loop();
|
||||
_webApiHuaweiClass.loop();
|
||||
_webApiWsPylontechLive.loop();
|
||||
_webApiPylontechClass.loop();
|
||||
}
|
||||
|
||||
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
|
||||
@ -83,7 +91,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
|
||||
|
||||
// WebAPI should set the X-Requested-With to prevent browser internal auth dialogs
|
||||
if (!request->hasHeader("X-Requested-With")) {
|
||||
r->addHeader(F("WWW-Authenticate"), F("Basic realm=\"Login Required\""));
|
||||
r->addHeader("WWW-Authenticate", "Basic realm=\"Login Required\"");
|
||||
}
|
||||
request->send(r);
|
||||
|
||||
|
||||
269
src/WebApi_Huawei.cpp
Normal file
269
src/WebApi_Huawei.cpp
Normal file
@ -0,0 +1,269 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Huawei.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "PinMapping.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiHuaweiClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1));
|
||||
_server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1));
|
||||
_server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1));
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::getJsonData(JsonObject& root) {
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root[F("input_voltage")]["v"] = rp->input_voltage;
|
||||
root[F("input_voltage")]["u"] = "V";
|
||||
root[F("input_current")]["v"] = rp->input_current;
|
||||
root[F("input_current")]["u"] = "A";
|
||||
root[F("input_power")]["v"] = rp->input_power;
|
||||
root[F("input_power")]["u"] = "W";
|
||||
root[F("output_voltage")]["v"] = rp->output_voltage;
|
||||
root[F("output_voltage")]["u"] = "V";
|
||||
root[F("output_current")]["v"] = rp->output_current;
|
||||
root[F("output_current")]["u"] = "A";
|
||||
root[F("max_output_current")]["v"] = rp->max_output_current;
|
||||
root[F("max_output_current")]["u"] = "A";
|
||||
root[F("output_power")]["v"] = rp->output_power;
|
||||
root[F("output_power")]["u"] = "W";
|
||||
root[F("input_temp")]["v"] = rp->input_temp;
|
||||
root[F("input_temp")]["u"] = "°C";
|
||||
root[F("output_temp")]["v"] = rp->output_temp;
|
||||
root[F("output_temp")]["u"] = "°C";
|
||||
root[F("efficiency")]["v"] = rp->efficiency;
|
||||
root[F("efficiency")]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
float value;
|
||||
uint8_t online = true;
|
||||
float minimal_voltage;
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("online")) {
|
||||
online = root[F("online")].as<bool>();
|
||||
if (online) {
|
||||
minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE;
|
||||
} else {
|
||||
minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE;
|
||||
}
|
||||
} else {
|
||||
retMsg[F("message")] = F("Could not read info if data should be set for online/offline operation!");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("voltage_valid")) {
|
||||
if (root[F("voltage_valid")].as<bool>()) {
|
||||
if (root[F("voltage")].as<float>() < minimal_voltage || root[F("voltage")].as<float>() > 58) {
|
||||
retMsg[F("message")] = F("voltage not in range between 42 (online)/48 (offline and 58V !");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidLimit;
|
||||
retMsg[F("param")][F("max")] = 58;
|
||||
retMsg[F("param")][F("min")] = minimal_voltage;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root[F("voltage")].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.containsKey("current_valid")) {
|
||||
if (root[F("current_valid")].as<bool>()) {
|
||||
if (root[F("current")].as<float>() < 0 || root[F("current")].as<float>() > 60) {
|
||||
retMsg[F("message")] = F("current must be in range between 0 and 60!");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidLimit;
|
||||
retMsg[F("param")][F("max")] = 60;
|
||||
retMsg[F("param")][F("min")] = 0;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
} else {
|
||||
value = root[F("current")].as<float>();
|
||||
if (online) {
|
||||
HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT);
|
||||
} else {
|
||||
HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("enabled")] = config.Huawei_Enabled;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("enabled"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Huawei_Enabled = root[F("enabled")].as<bool>();
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
if (config.Huawei_Enabled) {
|
||||
MessageOutput.println(F("Initialize Huawei AC charger interface... "));
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.init(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println(F("done"));
|
||||
} else {
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
}
|
||||
}
|
||||
|
||||
HuaweiCan.setPower(config.Huawei_Enabled);
|
||||
}
|
||||
87
src/WebApi_Pylontech.cpp
Normal file
87
src/WebApi_Pylontech.cpp
Normal file
@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_Pylontech.h"
|
||||
#include "Battery.h"
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "WebApi_errors.h"
|
||||
#include <AsyncJson.h>
|
||||
#include <Hoymiles.h>
|
||||
|
||||
void WebApiPylontechClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
|
||||
_server = server;
|
||||
|
||||
_server->on("/api/battery/livedata", HTTP_GET, std::bind(&WebApiPylontechClass::onStatus, this, _1));
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::loop()
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::getJsonData(JsonObject& root) {
|
||||
|
||||
root["data_age"] = (millis() - Battery.lastUpdate) / 1000;
|
||||
|
||||
root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ;
|
||||
root[F("chargeVoltage")]["u"] = "V";
|
||||
root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ;
|
||||
root[F("chargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ;
|
||||
root[F("dischargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ;
|
||||
root[F("stateOfCharge")]["u"] = "%";
|
||||
root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ;
|
||||
root[F("stateOfHealth")]["u"] = "%";
|
||||
root[F("voltage")]["v"] = Battery.voltage;
|
||||
root[F("voltage")]["u"] = "V";
|
||||
root[F("current")]["v"] = Battery.current ;
|
||||
root[F("current")]["u"] = "A";
|
||||
root[F("temperature")]["v"] = Battery.temperature ;
|
||||
root[F("temperature")]["u"] = "°C";
|
||||
|
||||
// Alarms
|
||||
root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ;
|
||||
root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ;
|
||||
root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ;
|
||||
root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ;
|
||||
root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ;
|
||||
root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ;
|
||||
root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ;
|
||||
|
||||
// Warnings
|
||||
root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ;
|
||||
root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ;
|
||||
root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ;
|
||||
root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ;
|
||||
root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ;
|
||||
root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ;
|
||||
root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ;
|
||||
|
||||
// Misc
|
||||
root[F("manufacturer")] = Battery.manufacturer ;
|
||||
root[F("chargeEnabled")] = Battery.chargeEnabled ;
|
||||
root[F("dischargeEnabled")] = Battery.dischargeEnabled ;
|
||||
root[F("chargeImmediately")] = Battery.chargeImmediately ;
|
||||
|
||||
}
|
||||
|
||||
void WebApiPylontechClass::onStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
getJsonData(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
@ -59,11 +59,11 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -72,8 +72,8 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -83,32 +83,32 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("delete"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("delete")].as<bool>() == false) {
|
||||
retMsg[F("message")] = F("Not deleted anything!");
|
||||
retMsg[F("code")] = WebApiError::ConfigNotDeleted;
|
||||
if (root["delete"].as<bool>() == false) {
|
||||
retMsg["message"] = "Not deleted anything!";
|
||||
retMsg["code"] = WebApiError::ConfigNotDeleted;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Configuration resettet. Rebooting now...");
|
||||
retMsg[F("code")] = WebApiError::ConfigSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Configuration resettet. Rebooting now...";
|
||||
retMsg["code"] = WebApiError::ConfigSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -125,7 +125,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
JsonArray data = root.createNestedArray(F("configs"));
|
||||
JsonArray data = root.createNestedArray("configs");
|
||||
|
||||
File rootfs = LittleFS.open("/");
|
||||
File file = rootfs.openNextFile();
|
||||
|
||||
@ -37,37 +37,41 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
|
||||
JsonObject curPin = root.createNestedObject("curPin");
|
||||
curPin[F("name")] = config.Dev_PinMapping;
|
||||
curPin["name"] = config.Dev_PinMapping;
|
||||
|
||||
JsonObject nrfPinObj = curPin.createNestedObject("nrf24");
|
||||
nrfPinObj[F("clk")] = pin.nrf24_clk;
|
||||
nrfPinObj[F("cs")] = pin.nrf24_cs;
|
||||
nrfPinObj[F("en")] = pin.nrf24_en;
|
||||
nrfPinObj[F("irq")] = pin.nrf24_irq;
|
||||
nrfPinObj[F("miso")] = pin.nrf24_miso;
|
||||
nrfPinObj[F("mosi")] = pin.nrf24_mosi;
|
||||
nrfPinObj["clk"] = pin.nrf24_clk;
|
||||
nrfPinObj["cs"] = pin.nrf24_cs;
|
||||
nrfPinObj["en"] = pin.nrf24_en;
|
||||
nrfPinObj["irq"] = pin.nrf24_irq;
|
||||
nrfPinObj["miso"] = pin.nrf24_miso;
|
||||
nrfPinObj["mosi"] = pin.nrf24_mosi;
|
||||
|
||||
JsonObject ethPinObj = curPin.createNestedObject("eth");
|
||||
ethPinObj[F("enabled")] = pin.eth_enabled;
|
||||
ethPinObj[F("phy_addr")] = pin.eth_phy_addr;
|
||||
ethPinObj[F("power")] = pin.eth_power;
|
||||
ethPinObj[F("mdc")] = pin.eth_mdc;
|
||||
ethPinObj[F("mdio")] = pin.eth_mdio;
|
||||
ethPinObj[F("type")] = pin.eth_type;
|
||||
ethPinObj[F("clk_mode")] = pin.eth_clk_mode;
|
||||
ethPinObj["enabled"] = pin.eth_enabled;
|
||||
ethPinObj["phy_addr"] = pin.eth_phy_addr;
|
||||
ethPinObj["power"] = pin.eth_power;
|
||||
ethPinObj["mdc"] = pin.eth_mdc;
|
||||
ethPinObj["mdio"] = pin.eth_mdio;
|
||||
ethPinObj["type"] = pin.eth_type;
|
||||
ethPinObj["clk_mode"] = pin.eth_clk_mode;
|
||||
|
||||
JsonObject displayPinObj = curPin.createNestedObject("display");
|
||||
displayPinObj[F("type")] = pin.display_type;
|
||||
displayPinObj[F("data")] = pin.display_data;
|
||||
displayPinObj[F("clk")] = pin.display_clk;
|
||||
displayPinObj[F("cs")] = pin.display_cs;
|
||||
displayPinObj[F("reset")] = pin.display_reset;
|
||||
displayPinObj["type"] = pin.display_type;
|
||||
displayPinObj["data"] = pin.display_data;
|
||||
displayPinObj["clk"] = pin.display_clk;
|
||||
displayPinObj["cs"] = pin.display_cs;
|
||||
displayPinObj["reset"] = pin.display_reset;
|
||||
|
||||
JsonObject ledPinObj = curPin.createNestedObject("led");
|
||||
ledPinObj["led0"] = pin.led[0];
|
||||
ledPinObj["led1"] = pin.led[1];
|
||||
|
||||
JsonObject display = root.createNestedObject("display");
|
||||
display[F("rotation")] = config.Display_Rotation;
|
||||
display[F("power_safe")] = config.Display_PowerSafe;
|
||||
display[F("screensaver")] = config.Display_ScreenSaver;
|
||||
display[F("contrast")] = config.Display_Contrast;
|
||||
display["rotation"] = config.Display_Rotation;
|
||||
display["power_safe"] = config.Display_PowerSafe;
|
||||
display["screensaver"] = config.Display_ScreenSaver;
|
||||
display["contrast"] = config.Display_Contrast;
|
||||
|
||||
JsonObject victronPinObj = curPin.createNestedObject("victron");
|
||||
victronPinObj[F("rx")] = pin.victron_rx;
|
||||
@ -77,6 +81,14 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
|
||||
batteryPinObj[F("rx")] = pin.battery_rx;
|
||||
batteryPinObj[F("tx")] = pin.battery_tx;
|
||||
|
||||
JsonObject huaweiPinObj = curPin.createNestedObject("huawei");
|
||||
huaweiPinObj[F("miso")] = pin.huawei_miso;
|
||||
huaweiPinObj[F("mosi")] = pin.huawei_mosi;
|
||||
huaweiPinObj[F("clk")] = pin.huawei_clk;
|
||||
huaweiPinObj[F("irq")] = pin.huawei_irq;
|
||||
huaweiPinObj[F("cs")] = pin.huawei_cs;
|
||||
huaweiPinObj[F("power")] = pin.huawei_power;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
}
|
||||
@ -89,11 +101,11 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -102,8 +114,8 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -113,38 +125,38 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("curPin") || root.containsKey("display"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("curPin")][F("name")].as<String>().length() == 0 || root[F("curPin")][F("name")].as<String>().length() > DEV_MAX_MAPPING_NAME_STRLEN) {
|
||||
retMsg[F("message")] = F("Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::HardwarePinMappingLength;
|
||||
retMsg[F("param")][F("max")] = DEV_MAX_MAPPING_NAME_STRLEN;
|
||||
if (root["curPin"]["name"].as<String>().length() == 0 || root["curPin"]["name"].as<String>().length() > DEV_MAX_MAPPING_NAME_STRLEN) {
|
||||
retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::HardwarePinMappingLength;
|
||||
retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
bool performRestart = root[F("curPin")][F("name")].as<String>() != config.Dev_PinMapping;
|
||||
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
|
||||
|
||||
strlcpy(config.Dev_PinMapping, root[F("curPin")][F("name")].as<String>().c_str(), sizeof(config.Dev_PinMapping));
|
||||
config.Display_Rotation = root[F("display")][F("rotation")].as<uint8_t>();
|
||||
config.Display_PowerSafe = root[F("display")][F("power_safe")].as<bool>();
|
||||
config.Display_ScreenSaver = root[F("display")][F("screensaver")].as<bool>();
|
||||
config.Display_Contrast = root[F("display")][F("contrast")].as<uint8_t>();
|
||||
strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping));
|
||||
config.Display_Rotation = root["display"]["rotation"].as<uint8_t>();
|
||||
config.Display_PowerSafe = root["display"]["power_safe"].as<bool>();
|
||||
config.Display_ScreenSaver = root["display"]["screensaver"].as<bool>();
|
||||
config.Display_Contrast = root["display"]["contrast"].as<uint8_t>();
|
||||
|
||||
Display.setOrientation(config.Display_Rotation);
|
||||
Display.enablePowerSafe = config.Display_PowerSafe;
|
||||
@ -153,9 +165,9 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -34,18 +34,18 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
|
||||
auto inv = Hoymiles.getInverterByPos(i);
|
||||
|
||||
JsonObject devInfoObj = root[inv->serialString()].createNestedObject();
|
||||
devInfoObj[F("valid_data")] = inv->DevInfo()->getLastUpdate() > 0;
|
||||
devInfoObj[F("fw_bootloader_version")] = inv->DevInfo()->getFwBootloaderVersion();
|
||||
devInfoObj[F("fw_build_version")] = inv->DevInfo()->getFwBuildVersion();
|
||||
devInfoObj[F("hw_part_number")] = inv->DevInfo()->getHwPartNumber();
|
||||
devInfoObj[F("hw_version")] = inv->DevInfo()->getHwVersion();
|
||||
devInfoObj[F("hw_model_name")] = inv->DevInfo()->getHwModelName();
|
||||
devInfoObj[F("max_power")] = inv->DevInfo()->getMaxPower();
|
||||
devInfoObj["valid_data"] = inv->DevInfo()->getLastUpdate() > 0;
|
||||
devInfoObj["fw_bootloader_version"] = inv->DevInfo()->getFwBootloaderVersion();
|
||||
devInfoObj["fw_build_version"] = inv->DevInfo()->getFwBuildVersion();
|
||||
devInfoObj["hw_part_number"] = inv->DevInfo()->getHwPartNumber();
|
||||
devInfoObj["hw_version"] = inv->DevInfo()->getHwVersion();
|
||||
devInfoObj["hw_model_name"] = inv->DevInfo()->getHwModelName();
|
||||
devInfoObj["max_power"] = inv->DevInfo()->getMaxPower();
|
||||
|
||||
char timebuffer[32];
|
||||
const time_t t = inv->DevInfo()->getFwBuildDateTime();
|
||||
std::strftime(timebuffer, sizeof(timebuffer), "%Y-%m-%d %H:%M:%S", gmtime(&t));
|
||||
devInfoObj[F("fw_build_datetime")] = String(timebuffer);
|
||||
devInfoObj["fw_build_datetime"] = String(timebuffer);
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
|
||||
@ -38,9 +38,9 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
|
||||
snprintf(buffer, sizeof(buffer), "%0x%08x",
|
||||
((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF)));
|
||||
root[F("dtu_serial")] = buffer;
|
||||
root[F("dtu_pollinterval")] = config.Dtu_PollInterval;
|
||||
root[F("dtu_palevel")] = config.Dtu_PaLevel;
|
||||
root["dtu_serial"] = buffer;
|
||||
root["dtu_pollinterval"] = config.Dtu_PollInterval;
|
||||
root["dtu_palevel"] = config.Dtu_PaLevel;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -54,11 +54,11 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -67,8 +67,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -78,40 +78,40 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("dtu_serial") && root.containsKey("dtu_pollinterval") && root.containsKey("dtu_palevel"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("dtu_serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial cannot be zero!");
|
||||
retMsg[F("code")] = WebApiError::DtuSerialZero;
|
||||
if (root["dtu_serial"].as<uint64_t>() == 0) {
|
||||
retMsg["message"] = "Serial cannot be zero!";
|
||||
retMsg["code"] = WebApiError::DtuSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("dtu_pollinterval")].as<uint32_t>() == 0) {
|
||||
retMsg[F("message")] = F("Poll interval must be greater zero!");
|
||||
retMsg[F("code")] = WebApiError::DtuPollZero;
|
||||
if (root["dtu_pollinterval"].as<uint32_t>() == 0) {
|
||||
retMsg["message"] = "Poll interval must be greater zero!";
|
||||
retMsg["code"] = WebApiError::DtuPollZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("dtu_palevel")].as<uint8_t>() > 3) {
|
||||
retMsg[F("message")] = F("Invalid power level setting!");
|
||||
retMsg[F("code")] = WebApiError::DtuInvalidPowerLevel;
|
||||
if (root["dtu_palevel"].as<uint8_t>() > 3) {
|
||||
retMsg["message"] = "Invalid power level setting!";
|
||||
retMsg["code"] = WebApiError::DtuInvalidPowerLevel;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -120,14 +120,14 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
|
||||
CONFIG_T& config = Configuration.get();
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
config.Dtu_Serial = strtoll(root[F("dtu_serial")].as<String>().c_str(), NULL, 16);
|
||||
config.Dtu_PollInterval = root[F("dtu_pollinterval")].as<uint32_t>();
|
||||
config.Dtu_PaLevel = root[F("dtu_palevel")].as<uint8_t>();
|
||||
config.Dtu_Serial = strtoll(root["dtu_serial"].as<String>().c_str(), NULL, 16);
|
||||
config.Dtu_PollInterval = root["dtu_pollinterval"].as<uint32_t>();
|
||||
config.Dtu_PaLevel = root["dtu_palevel"].as<uint8_t>();
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -43,7 +43,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
uint8_t logEntryCount = inv->EventLog()->getEntryCount();
|
||||
|
||||
root[serial]["count"] = logEntryCount;
|
||||
JsonArray eventsArray = root[serial].createNestedArray(F("events"));
|
||||
JsonArray eventsArray = root[serial].createNestedArray("events");
|
||||
|
||||
for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) {
|
||||
JsonObject eventsObject = eventsArray.createNestedObject();
|
||||
@ -51,10 +51,10 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request)
|
||||
AlarmLogEntry_t entry;
|
||||
inv->EventLog()->getLogEntry(logEntry, &entry);
|
||||
|
||||
eventsObject[F("message_id")] = entry.MessageId;
|
||||
eventsObject[F("message")] = entry.Message;
|
||||
eventsObject[F("start_time")] = entry.StartTime;
|
||||
eventsObject[F("end_time")] = entry.EndTime;
|
||||
eventsObject["message_id"] = entry.MessageId;
|
||||
eventsObject["message"] = entry.Message;
|
||||
eventsObject["start_time"] = entry.StartTime;
|
||||
eventsObject["end_time"] = entry.EndTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,34 +35,34 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U);
|
||||
JsonObject root = response->getRoot();
|
||||
JsonArray data = root.createNestedArray(F("inverter"));
|
||||
JsonArray data = root.createNestedArray("inverter");
|
||||
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
|
||||
if (config.Inverter[i].Serial > 0) {
|
||||
JsonObject obj = data.createNestedObject();
|
||||
obj[F("id")] = i;
|
||||
obj[F("name")] = String(config.Inverter[i].Name);
|
||||
obj["id"] = i;
|
||||
obj["name"] = String(config.Inverter[i].Name);
|
||||
|
||||
// Inverter Serial is read as HEX
|
||||
char buffer[sizeof(uint64_t) * 8 + 1];
|
||||
snprintf(buffer, sizeof(buffer), "%0x%08x",
|
||||
((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)),
|
||||
((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF)));
|
||||
obj[F("serial")] = buffer;
|
||||
obj[F("poll_enable")] = config.Inverter[i].Poll_Enable;
|
||||
obj[F("poll_enable_night")] = config.Inverter[i].Poll_Enable_Night;
|
||||
obj[F("command_enable")] = config.Inverter[i].Command_Enable;
|
||||
obj[F("command_enable_night")] = config.Inverter[i].Command_Enable_Night;
|
||||
obj["serial"] = buffer;
|
||||
obj["poll_enable"] = config.Inverter[i].Poll_Enable;
|
||||
obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
|
||||
obj["command_enable"] = config.Inverter[i].Command_Enable;
|
||||
obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
|
||||
uint8_t max_channels;
|
||||
if (inv == nullptr) {
|
||||
obj[F("type")] = F("Unknown");
|
||||
obj["type"] = "Unknown";
|
||||
max_channels = INV_MAX_CHAN_COUNT;
|
||||
} else {
|
||||
obj[F("type")] = inv->typeName();
|
||||
obj["type"] = inv->typeName();
|
||||
max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size();
|
||||
}
|
||||
|
||||
@ -88,11 +88,11 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -101,8 +101,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -112,33 +112,33 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("serial") && root.containsKey("name"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial must be a number > 0!");
|
||||
retMsg[F("code")] = WebApiError::InverterSerialZero;
|
||||
if (root["serial"].as<uint64_t>() == 0) {
|
||||
retMsg["message"] = "Serial must be a number > 0!";
|
||||
retMsg["code"] = WebApiError::InverterSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("name")].as<String>().length() == 0 || root[F("name")].as<String>().length() > INV_MAX_NAME_STRLEN) {
|
||||
retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::InverterNameLength;
|
||||
retMsg[F("param")][F("max")] = INV_MAX_NAME_STRLEN;
|
||||
if (root["name"].as<String>().length() == 0 || root["name"].as<String>().length() > INV_MAX_NAME_STRLEN) {
|
||||
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::InverterNameLength;
|
||||
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -147,23 +147,23 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
|
||||
INVERTER_CONFIG_T* inverter = Configuration.getFreeInverterSlot();
|
||||
|
||||
if (!inverter) {
|
||||
retMsg[F("message")] = F("Only " STR(INV_MAX_COUNT) " inverters are supported!");
|
||||
retMsg[F("code")] = WebApiError::InverterCount;
|
||||
retMsg[F("param")][F("max")] = INV_MAX_COUNT;
|
||||
retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!";
|
||||
retMsg["code"] = WebApiError::InverterCount;
|
||||
retMsg["param"]["max"] = INV_MAX_COUNT;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
inverter->Serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
inverter->Serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
|
||||
strncpy(inverter->Name, root[F("name")].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
strncpy(inverter->Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Inverter created!");
|
||||
retMsg[F("code")] = WebApiError::InverterAdded;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Inverter created!";
|
||||
retMsg["code"] = WebApiError::InverterAdded;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -187,11 +187,11 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -200,8 +200,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -211,82 +211,82 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("id")].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg[F("message")] = F("Invalid ID specified!");
|
||||
retMsg[F("code")] = WebApiError::InverterInvalidId;
|
||||
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg["message"] = "Invalid ID specified!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidId;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial must be a number > 0!");
|
||||
retMsg[F("code")] = WebApiError::InverterSerialZero;
|
||||
if (root["serial"].as<uint64_t>() == 0) {
|
||||
retMsg["message"] = "Serial must be a number > 0!";
|
||||
retMsg["code"] = WebApiError::InverterSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("name")].as<String>().length() == 0 || root[F("name")].as<String>().length() > INV_MAX_NAME_STRLEN) {
|
||||
retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::InverterNameLength;
|
||||
retMsg[F("param")][F("max")] = INV_MAX_NAME_STRLEN;
|
||||
if (root["name"].as<String>().length() == 0 || root["name"].as<String>().length() > INV_MAX_NAME_STRLEN) {
|
||||
retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::InverterNameLength;
|
||||
retMsg["param"]["max"] = INV_MAX_NAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray channelArray = root[F("channel")].as<JsonArray>();
|
||||
JsonArray channelArray = root["channel"].as<JsonArray>();
|
||||
if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) {
|
||||
retMsg[F("message")] = F("Invalid amount of max channel setting given!");
|
||||
retMsg[F("code")] = WebApiError::InverterInvalidMaxChannel;
|
||||
retMsg["message"] = "Invalid amount of max channel setting given!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidMaxChannel;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as<uint8_t>()];
|
||||
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];
|
||||
|
||||
uint64_t new_serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
uint64_t new_serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
uint64_t old_serial = inverter.Serial;
|
||||
|
||||
// Interpret the string as a hex value and convert it to uint64_t
|
||||
inverter.Serial = new_serial;
|
||||
strncpy(inverter.Name, root[F("name")].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
strncpy(inverter.Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
|
||||
|
||||
uint8_t arrayCount = 0;
|
||||
for (JsonVariant channel : channelArray) {
|
||||
inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as<uint16_t>();
|
||||
inverter.channel[arrayCount].YieldTotalOffset = channel[F("yield_total_offset")].as<float>();
|
||||
strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name));
|
||||
inverter.Poll_Enable = root[F("poll_enable")] | true;
|
||||
inverter.Poll_Enable_Night = root[F("poll_enable_night")] | true;
|
||||
inverter.Command_Enable = root[F("command_enable")] | true;
|
||||
inverter.Command_Enable_Night = root[F("command_enable_night")] | true;
|
||||
inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as<uint16_t>();
|
||||
inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as<float>();
|
||||
strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name));
|
||||
inverter.Poll_Enable = root["poll_enable"] | true;
|
||||
inverter.Poll_Enable_Night = root["poll_enable_night"] | true;
|
||||
inverter.Command_Enable = root["command_enable"] | true;
|
||||
inverter.Command_Enable_Night = root["command_enable_night"] | true;
|
||||
|
||||
arrayCount++;
|
||||
}
|
||||
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("code")] = WebApiError::InverterChanged;
|
||||
retMsg[F("message")] = F("Inverter changed!");
|
||||
retMsg["type"] = "success";
|
||||
retMsg["code"] = WebApiError::InverterChanged;
|
||||
retMsg["message"] = "Inverter changed!";
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -325,11 +325,11 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -338,8 +338,8 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -349,30 +349,30 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("id"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("id")].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg[F("message")] = F("Invalid ID specified!");
|
||||
retMsg[F("code")] = WebApiError::InverterInvalidId;
|
||||
if (root["id"].as<uint8_t>() > INV_MAX_COUNT - 1) {
|
||||
retMsg["message"] = "Invalid ID specified!";
|
||||
retMsg["code"] = WebApiError::InverterInvalidId;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t inverter_id = root[F("id")].as<uint8_t>();
|
||||
uint8_t inverter_id = root["id"].as<uint8_t>();
|
||||
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id];
|
||||
|
||||
Hoymiles.removeInverterBySerial(inverter.Serial);
|
||||
@ -381,9 +381,9 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
|
||||
strncpy(inverter.Name, "", sizeof(inverter.Name));
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Inverter deleted!");
|
||||
retMsg[F("code")] = WebApiError::InverterDeleted;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Inverter deleted!";
|
||||
retMsg["code"] = WebApiError::InverterDeleted;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -63,11 +63,11 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -76,8 +76,8 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -87,8 +87,8 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -97,50 +97,50 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
if (!(root.containsKey("serial")
|
||||
&& root.containsKey("limit_value")
|
||||
&& root.containsKey("limit_type"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial must be a number > 0!");
|
||||
retMsg[F("code")] = WebApiError::LimitSerialZero;
|
||||
if (root["serial"].as<uint64_t>() == 0) {
|
||||
retMsg["message"] = "Serial must be a number > 0!";
|
||||
retMsg["code"] = WebApiError::LimitSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("limit_value")].as<uint16_t>() == 0 || root[F("limit_value")].as<uint16_t>() > 1500) {
|
||||
retMsg[F("message")] = F("Limit must between 1 and 1500!");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidLimit;
|
||||
retMsg[F("param")][F("max")] = 1500;
|
||||
if (root["limit_value"].as<uint16_t>() == 0 || root["limit_value"].as<uint16_t>() > 1500) {
|
||||
retMsg["message"] = "Limit must between 1 and 1500!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidLimit;
|
||||
retMsg["param"]["max"] = 1500;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!((root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::AbsolutNonPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::AbsolutPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::RelativNonPersistent)
|
||||
|| (root[F("limit_type")].as<uint16_t>() == PowerLimitControlType::RelativPersistent))) {
|
||||
if (!((root["limit_type"].as<uint16_t>() == PowerLimitControlType::AbsolutNonPersistent)
|
||||
|| (root["limit_type"].as<uint16_t>() == PowerLimitControlType::AbsolutPersistent)
|
||||
|| (root["limit_type"].as<uint16_t>() == PowerLimitControlType::RelativNonPersistent)
|
||||
|| (root["limit_type"].as<uint16_t>() == PowerLimitControlType::RelativPersistent))) {
|
||||
|
||||
retMsg[F("message")] = F("Invalid type specified!");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidType;
|
||||
retMsg["message"] = "Invalid type specified!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidType;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
uint16_t limit = root[F("limit_value")].as<uint16_t>();
|
||||
PowerLimitControlType type = root[F("limit_type")].as<PowerLimitControlType>();
|
||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
uint16_t limit = root["limit_value"].as<uint16_t>();
|
||||
PowerLimitControlType type = root["limit_type"].as<PowerLimitControlType>();
|
||||
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
if (inv == nullptr) {
|
||||
retMsg[F("message")] = F("Invalid inverter specified!");
|
||||
retMsg[F("code")] = WebApiError::LimitInvalidInverter;
|
||||
retMsg["message"] = "Invalid inverter specified!";
|
||||
retMsg["code"] = WebApiError::LimitInvalidInverter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -148,9 +148,9 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request)
|
||||
|
||||
inv->sendActivePowerControlRequest(Hoymiles.getRadio(), limit, type);
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -29,11 +29,11 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -42,8 +42,8 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -53,25 +53,25 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("reboot"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("reboot")].as<bool>()) {
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Reboot triggered!");
|
||||
retMsg[F("code")] = WebApiError::MaintenanceRebootTriggered;
|
||||
if (root["reboot"].as<bool>()) {
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Reboot triggered!";
|
||||
retMsg["code"] = WebApiError::MaintenanceRebootTriggered;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -80,8 +80,8 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
|
||||
yield();
|
||||
ESP.restart();
|
||||
} else {
|
||||
retMsg[F("message")] = F("Reboot cancled!");
|
||||
retMsg[F("code")] = WebApiError::MaintenanceRebootCancled;
|
||||
retMsg["message"] = "Reboot cancled!";
|
||||
retMsg["code"] = WebApiError::MaintenanceRebootCancled;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -39,22 +39,22 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
|
||||
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
|
||||
root[F("mqtt_port")] = config.Mqtt_Port;
|
||||
root[F("mqtt_username")] = config.Mqtt_Username;
|
||||
root[F("mqtt_topic")] = config.Mqtt_Topic;
|
||||
root[F("mqtt_connected")] = MqttSettings.getConnected();
|
||||
root[F("mqtt_retain")] = config.Mqtt_Retain;
|
||||
root[F("mqtt_tls")] = config.Mqtt_Tls;
|
||||
root[F("mqtt_root_ca_cert_info")] = getRootCaCertInfo(config.Mqtt_RootCaCert);
|
||||
root[F("mqtt_lwt_topic")] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic;
|
||||
root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval;
|
||||
root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled;
|
||||
root[F("mqtt_hass_expire")] = config.Mqtt_Hass_Expire;
|
||||
root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain;
|
||||
root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic;
|
||||
root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels;
|
||||
root["mqtt_enabled"] = config.Mqtt_Enabled;
|
||||
root["mqtt_hostname"] = config.Mqtt_Hostname;
|
||||
root["mqtt_port"] = config.Mqtt_Port;
|
||||
root["mqtt_username"] = config.Mqtt_Username;
|
||||
root["mqtt_topic"] = config.Mqtt_Topic;
|
||||
root["mqtt_connected"] = MqttSettings.getConnected();
|
||||
root["mqtt_retain"] = config.Mqtt_Retain;
|
||||
root["mqtt_tls"] = config.Mqtt_Tls;
|
||||
root["mqtt_root_ca_cert_info"] = getRootCaCertInfo(config.Mqtt_RootCaCert);
|
||||
root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic;
|
||||
root["mqtt_publish_interval"] = config.Mqtt_PublishInterval;
|
||||
root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled;
|
||||
root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire;
|
||||
root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain;
|
||||
root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic;
|
||||
root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -70,24 +70,24 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("mqtt_enabled")] = config.Mqtt_Enabled;
|
||||
root[F("mqtt_hostname")] = config.Mqtt_Hostname;
|
||||
root[F("mqtt_port")] = config.Mqtt_Port;
|
||||
root[F("mqtt_username")] = config.Mqtt_Username;
|
||||
root[F("mqtt_password")] = config.Mqtt_Password;
|
||||
root[F("mqtt_topic")] = config.Mqtt_Topic;
|
||||
root[F("mqtt_retain")] = config.Mqtt_Retain;
|
||||
root[F("mqtt_tls")] = config.Mqtt_Tls;
|
||||
root[F("mqtt_root_ca_cert")] = config.Mqtt_RootCaCert;
|
||||
root[F("mqtt_lwt_topic")] = config.Mqtt_LwtTopic;
|
||||
root[F("mqtt_lwt_online")] = config.Mqtt_LwtValue_Online;
|
||||
root[F("mqtt_lwt_offline")] = config.Mqtt_LwtValue_Offline;
|
||||
root[F("mqtt_publish_interval")] = config.Mqtt_PublishInterval;
|
||||
root[F("mqtt_hass_enabled")] = config.Mqtt_Hass_Enabled;
|
||||
root[F("mqtt_hass_expire")] = config.Mqtt_Hass_Expire;
|
||||
root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain;
|
||||
root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic;
|
||||
root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels;
|
||||
root["mqtt_enabled"] = config.Mqtt_Enabled;
|
||||
root["mqtt_hostname"] = config.Mqtt_Hostname;
|
||||
root["mqtt_port"] = config.Mqtt_Port;
|
||||
root["mqtt_username"] = config.Mqtt_Username;
|
||||
root["mqtt_password"] = config.Mqtt_Password;
|
||||
root["mqtt_topic"] = config.Mqtt_Topic;
|
||||
root["mqtt_retain"] = config.Mqtt_Retain;
|
||||
root["mqtt_tls"] = config.Mqtt_Tls;
|
||||
root["mqtt_root_ca_cert"] = config.Mqtt_RootCaCert;
|
||||
root["mqtt_lwt_topic"] = config.Mqtt_LwtTopic;
|
||||
root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online;
|
||||
root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline;
|
||||
root["mqtt_publish_interval"] = config.Mqtt_PublishInterval;
|
||||
root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled;
|
||||
root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire;
|
||||
root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain;
|
||||
root["mqtt_hass_topic"] = config.Mqtt_Hass_Topic;
|
||||
root["mqtt_hass_individualpanels"] = config.Mqtt_Hass_IndividualPanels;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -101,11 +101,11 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE);
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -114,8 +114,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > MQTT_JSON_DOC_SIZE) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -125,8 +125,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -149,139 +149,139 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("mqtt_hass_retain")
|
||||
&& root.containsKey("mqtt_hass_topic")
|
||||
&& root.containsKey("mqtt_hass_individualpanels"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_enabled")].as<bool>()) {
|
||||
if (root[F("mqtt_hostname")].as<String>().length() == 0 || root[F("mqtt_hostname")].as<String>().length() > MQTT_MAX_HOSTNAME_STRLEN) {
|
||||
retMsg[F("message")] = F("MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::MqttHostnameLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_HOSTNAME_STRLEN;
|
||||
if (root["mqtt_enabled"].as<bool>()) {
|
||||
if (root["mqtt_hostname"].as<String>().length() == 0 || root["mqtt_hostname"].as<String>().length() > MQTT_MAX_HOSTNAME_STRLEN) {
|
||||
retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::MqttHostnameLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_username")].as<String>().length() > MQTT_MAX_USERNAME_STRLEN) {
|
||||
retMsg[F("message")] = F("Username must not longer then " STR(MQTT_MAX_USERNAME_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttUsernameLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_USERNAME_STRLEN;
|
||||
if (root["mqtt_username"].as<String>().length() > MQTT_MAX_USERNAME_STRLEN) {
|
||||
retMsg["message"] = "Username must not longer then " STR(MQTT_MAX_USERNAME_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttUsernameLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
if (root[F("mqtt_password")].as<String>().length() > MQTT_MAX_PASSWORD_STRLEN) {
|
||||
retMsg[F("message")] = F("Password must not longer then " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttPasswordLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_PASSWORD_STRLEN;
|
||||
if (root["mqtt_password"].as<String>().length() > MQTT_MAX_PASSWORD_STRLEN) {
|
||||
retMsg["message"] = "Password must not longer then " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttPasswordLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
if (root[F("mqtt_topic")].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg[F("message")] = F("Topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttTopicLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN;
|
||||
if (root["mqtt_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg["message"] = "Topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_topic")].as<String>().indexOf(' ') != -1) {
|
||||
retMsg[F("message")] = F("Topic must not contain space characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttTopicCharacter;
|
||||
if (root["mqtt_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "Topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root[F("mqtt_topic")].as<String>().endsWith("/")) {
|
||||
retMsg[F("message")] = F("Topic must end with slash (/)!");
|
||||
retMsg[F("code")] = WebApiError::MqttTopicTrailingSlash;
|
||||
if (!root["mqtt_topic"].as<String>().endsWith("/")) {
|
||||
retMsg["message"] = "Topic must end with slash (/)!";
|
||||
retMsg["code"] = WebApiError::MqttTopicTrailingSlash;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_port")].as<uint>() == 0 || root[F("mqtt_port")].as<uint>() > 65535) {
|
||||
retMsg[F("message")] = F("Port must be a number between 1 and 65535!");
|
||||
retMsg[F("code")] = WebApiError::MqttPort;
|
||||
if (root["mqtt_port"].as<uint>() == 0 || root["mqtt_port"].as<uint>() > 65535) {
|
||||
retMsg["message"] = "Port must be a number between 1 and 65535!";
|
||||
retMsg["code"] = WebApiError::MqttPort;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_root_ca_cert")].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN) {
|
||||
retMsg[F("message")] = F("Certificate must not longer then " STR(MQTT_MAX_ROOT_CA_CERT_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttCertificateLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_ROOT_CA_CERT_STRLEN;
|
||||
if (root["mqtt_root_ca_cert"].as<String>().length() > MQTT_MAX_ROOT_CA_CERT_STRLEN) {
|
||||
retMsg["message"] = "Certificate must not longer then " STR(MQTT_MAX_ROOT_CA_CERT_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttCertificateLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_ROOT_CA_CERT_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_lwt_topic")].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg[F("message")] = F("LWT topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttLwtTopicLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN;
|
||||
if (root["mqtt_lwt_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg["message"] = "LWT topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_lwt_topic")].as<String>().indexOf(' ') != -1) {
|
||||
retMsg[F("message")] = F("LWT topic must not contain space characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttLwtTopicCharacter;
|
||||
if (root["mqtt_lwt_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "LWT topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_lwt_online")].as<String>().length() > MQTT_MAX_LWTVALUE_STRLEN) {
|
||||
retMsg[F("message")] = F("LWT online value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttLwtOnlineLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
if (root["mqtt_lwt_online"].as<String>().length() > MQTT_MAX_LWTVALUE_STRLEN) {
|
||||
retMsg["message"] = "LWT online value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtOnlineLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_lwt_offline")].as<String>().length() > MQTT_MAX_LWTVALUE_STRLEN) {
|
||||
retMsg[F("message")] = F("LWT offline value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttLwtOfflineLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
if (root["mqtt_lwt_offline"].as<String>().length() > MQTT_MAX_LWTVALUE_STRLEN) {
|
||||
retMsg["message"] = "LWT offline value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttLwtOfflineLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_publish_interval")].as<uint32_t>() < 5 || root[F("mqtt_publish_interval")].as<uint32_t>() > 65535) {
|
||||
retMsg[F("message")] = F("Publish interval must be a number between 5 and 65535!");
|
||||
retMsg[F("code")] = WebApiError::MqttPublishInterval;
|
||||
retMsg[F("param")][F("min")] = 5;
|
||||
retMsg[F("param")][F("max")] = 65535;
|
||||
if (root["mqtt_publish_interval"].as<uint32_t>() < 5 || root["mqtt_publish_interval"].as<uint32_t>() > 65535) {
|
||||
retMsg["message"] = "Publish interval must be a number between 5 and 65535!";
|
||||
retMsg["code"] = WebApiError::MqttPublishInterval;
|
||||
retMsg["param"]["min"] = 5;
|
||||
retMsg["param"]["max"] = 65535;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_hass_enabled")].as<bool>()) {
|
||||
if (root[F("mqtt_hass_topic")].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg[F("message")] = F("Hass topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttHassTopicLength;
|
||||
retMsg[F("param")][F("max")] = MQTT_MAX_TOPIC_STRLEN;
|
||||
if (root["mqtt_hass_enabled"].as<bool>()) {
|
||||
if (root["mqtt_hass_topic"].as<String>().length() > MQTT_MAX_TOPIC_STRLEN) {
|
||||
retMsg["message"] = "Hass topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!";
|
||||
retMsg["code"] = WebApiError::MqttHassTopicLength;
|
||||
retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("mqtt_hass_topic")].as<String>().indexOf(' ') != -1) {
|
||||
retMsg[F("message")] = F("Hass topic must not contain space characters!");
|
||||
retMsg[F("code")] = WebApiError::MqttHassTopicCharacter;
|
||||
if (root["mqtt_hass_topic"].as<String>().indexOf(' ') != -1) {
|
||||
retMsg["message"] = "Hass topic must not contain space characters!";
|
||||
retMsg["code"] = WebApiError::MqttHassTopicCharacter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -290,29 +290,29 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.Mqtt_Enabled = root[F("mqtt_enabled")].as<bool>();
|
||||
config.Mqtt_Retain = root[F("mqtt_retain")].as<bool>();
|
||||
config.Mqtt_Tls = root[F("mqtt_tls")].as<bool>();
|
||||
strlcpy(config.Mqtt_RootCaCert, root[F("mqtt_root_ca_cert")].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert));
|
||||
config.Mqtt_Port = root[F("mqtt_port")].as<uint>();
|
||||
strlcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as<String>().c_str(), sizeof(config.Mqtt_Hostname));
|
||||
strlcpy(config.Mqtt_Username, root[F("mqtt_username")].as<String>().c_str(), sizeof(config.Mqtt_Username));
|
||||
strlcpy(config.Mqtt_Password, root[F("mqtt_password")].as<String>().c_str(), sizeof(config.Mqtt_Password));
|
||||
strlcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Mqtt_Topic));
|
||||
strlcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as<String>().c_str(), sizeof(config.Mqtt_LwtTopic));
|
||||
strlcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Online));
|
||||
strlcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Offline));
|
||||
config.Mqtt_PublishInterval = root[F("mqtt_publish_interval")].as<uint32_t>();
|
||||
config.Mqtt_Hass_Enabled = root[F("mqtt_hass_enabled")].as<bool>();
|
||||
config.Mqtt_Hass_Expire = root[F("mqtt_hass_expire")].as<bool>();
|
||||
config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as<bool>();
|
||||
config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as<bool>();
|
||||
strlcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as<String>().c_str(), sizeof(config.Mqtt_Hass_Topic));
|
||||
config.Mqtt_Enabled = root["mqtt_enabled"].as<bool>();
|
||||
config.Mqtt_Retain = root["mqtt_retain"].as<bool>();
|
||||
config.Mqtt_Tls = root["mqtt_tls"].as<bool>();
|
||||
strlcpy(config.Mqtt_RootCaCert, root["mqtt_root_ca_cert"].as<String>().c_str(), sizeof(config.Mqtt_RootCaCert));
|
||||
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_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_Topic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Mqtt_Topic));
|
||||
strlcpy(config.Mqtt_LwtTopic, root["mqtt_lwt_topic"].as<String>().c_str(), sizeof(config.Mqtt_LwtTopic));
|
||||
strlcpy(config.Mqtt_LwtValue_Online, root["mqtt_lwt_online"].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Online));
|
||||
strlcpy(config.Mqtt_LwtValue_Offline, root["mqtt_lwt_offline"].as<String>().c_str(), sizeof(config.Mqtt_LwtValue_Offline));
|
||||
config.Mqtt_PublishInterval = root["mqtt_publish_interval"].as<uint32_t>();
|
||||
config.Mqtt_Hass_Enabled = root["mqtt_hass_enabled"].as<bool>();
|
||||
config.Mqtt_Hass_Expire = root["mqtt_hass_expire"].as<bool>();
|
||||
config.Mqtt_Hass_Retain = root["mqtt_hass_retain"].as<bool>();
|
||||
config.Mqtt_Hass_IndividualPanels = root["mqtt_hass_individualpanels"].as<bool>();
|
||||
strlcpy(config.Mqtt_Hass_Topic, root["mqtt_hass_topic"].as<String>().c_str(), sizeof(config.Mqtt_Hass_Topic));
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -34,22 +34,22 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request)
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0);
|
||||
root[F("sta_ssid")] = WiFi.SSID();
|
||||
root[F("sta_rssi")] = WiFi.RSSI();
|
||||
root[F("network_hostname")] = NetworkSettings.getHostname();
|
||||
root[F("network_ip")] = NetworkSettings.localIP().toString();
|
||||
root[F("network_netmask")] = NetworkSettings.subnetMask().toString();
|
||||
root[F("network_gateway")] = NetworkSettings.gatewayIP().toString();
|
||||
root[F("network_dns1")] = NetworkSettings.dnsIP(0).toString();
|
||||
root[F("network_dns2")] = NetworkSettings.dnsIP(1).toString();
|
||||
root[F("network_mac")] = NetworkSettings.macAddress();
|
||||
root[F("network_mode")] = NetworkSettings.NetworkMode() == network_mode::WiFi ? F("Station") : F("Ethernet");
|
||||
root[F("ap_status")] = ((WiFi.getMode() & WIFI_AP) != 0);
|
||||
root[F("ap_ssid")] = NetworkSettings.getApName();
|
||||
root[F("ap_ip")] = WiFi.softAPIP().toString();
|
||||
root[F("ap_mac")] = WiFi.softAPmacAddress();
|
||||
root[F("ap_stationnum")] = WiFi.softAPgetStationNum();
|
||||
root["sta_status"] = ((WiFi.getMode() & WIFI_STA) != 0);
|
||||
root["sta_ssid"] = WiFi.SSID();
|
||||
root["sta_rssi"] = WiFi.RSSI();
|
||||
root["network_hostname"] = NetworkSettings.getHostname();
|
||||
root["network_ip"] = NetworkSettings.localIP().toString();
|
||||
root["network_netmask"] = NetworkSettings.subnetMask().toString();
|
||||
root["network_gateway"] = NetworkSettings.gatewayIP().toString();
|
||||
root["network_dns1"] = NetworkSettings.dnsIP(0).toString();
|
||||
root["network_dns2"] = NetworkSettings.dnsIP(1).toString();
|
||||
root["network_mac"] = NetworkSettings.macAddress();
|
||||
root["network_mode"] = NetworkSettings.NetworkMode() == network_mode::WiFi ? "Station" : "Ethernet";
|
||||
root["ap_status"] = ((WiFi.getMode() & WIFI_AP) != 0);
|
||||
root["ap_ssid"] = NetworkSettings.getApName();
|
||||
root["ap_ip"] = WiFi.softAPIP().toString();
|
||||
root["ap_mac"] = WiFi.softAPmacAddress();
|
||||
root["ap_stationnum"] = WiFi.softAPgetStationNum();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -65,15 +65,15 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("hostname")] = config.WiFi_Hostname;
|
||||
root[F("dhcp")] = config.WiFi_Dhcp;
|
||||
root[F("ipaddress")] = IPAddress(config.WiFi_Ip).toString();
|
||||
root[F("netmask")] = IPAddress(config.WiFi_Netmask).toString();
|
||||
root[F("gateway")] = IPAddress(config.WiFi_Gateway).toString();
|
||||
root[F("dns1")] = IPAddress(config.WiFi_Dns1).toString();
|
||||
root[F("dns2")] = IPAddress(config.WiFi_Dns2).toString();
|
||||
root[F("ssid")] = config.WiFi_Ssid;
|
||||
root[F("password")] = config.WiFi_Password;
|
||||
root["hostname"] = config.WiFi_Hostname;
|
||||
root["dhcp"] = config.WiFi_Dhcp;
|
||||
root["ipaddress"] = IPAddress(config.WiFi_Ip).toString();
|
||||
root["netmask"] = IPAddress(config.WiFi_Netmask).toString();
|
||||
root["gateway"] = IPAddress(config.WiFi_Gateway).toString();
|
||||
root["dns1"] = IPAddress(config.WiFi_Dns1).toString();
|
||||
root["dns2"] = IPAddress(config.WiFi_Dns2).toString();
|
||||
root["ssid"] = config.WiFi_Ssid;
|
||||
root["password"] = config.WiFi_Password;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -87,11 +87,11 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -100,8 +100,8 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -111,78 +111,78 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
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"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
IPAddress ipaddress;
|
||||
if (!ipaddress.fromString(root[F("ipaddress")].as<String>())) {
|
||||
retMsg[F("message")] = F("IP address is invalid!");
|
||||
retMsg[F("code")] = WebApiError::NetworkIpInvalid;
|
||||
if (!ipaddress.fromString(root["ipaddress"].as<String>())) {
|
||||
retMsg["message"] = "IP address is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkIpInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
IPAddress netmask;
|
||||
if (!netmask.fromString(root[F("netmask")].as<String>())) {
|
||||
retMsg[F("message")] = F("Netmask is invalid!");
|
||||
retMsg[F("code")] = WebApiError::NetworkNetmaskInvalid;
|
||||
if (!netmask.fromString(root["netmask"].as<String>())) {
|
||||
retMsg["message"] = "Netmask is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkNetmaskInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
IPAddress gateway;
|
||||
if (!gateway.fromString(root[F("gateway")].as<String>())) {
|
||||
retMsg[F("message")] = F("Gateway is invalid!");
|
||||
retMsg[F("code")] = WebApiError::NetworkGatewayInvalid;
|
||||
if (!gateway.fromString(root["gateway"].as<String>())) {
|
||||
retMsg["message"] = "Gateway is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkGatewayInvalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
IPAddress dns1;
|
||||
if (!dns1.fromString(root[F("dns1")].as<String>())) {
|
||||
retMsg[F("message")] = F("DNS Server IP 1 is invalid!");
|
||||
retMsg[F("code")] = WebApiError::NetworkDns1Invalid;
|
||||
if (!dns1.fromString(root["dns1"].as<String>())) {
|
||||
retMsg["message"] = "DNS Server IP 1 is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkDns1Invalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
IPAddress dns2;
|
||||
if (!dns2.fromString(root[F("dns2")].as<String>())) {
|
||||
retMsg[F("message")] = F("DNS Server IP 2 is invalid!");
|
||||
retMsg[F("code")] = WebApiError::NetworkDns2Invalid;
|
||||
if (!dns2.fromString(root["dns2"].as<String>())) {
|
||||
retMsg["message"] = "DNS Server IP 2 is invalid!";
|
||||
retMsg["code"] = WebApiError::NetworkDns2Invalid;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("hostname")].as<String>().length() == 0 || root[F("hostname")].as<String>().length() > WIFI_MAX_HOSTNAME_STRLEN) {
|
||||
retMsg[F("message")] = F("Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!");
|
||||
if (root["hostname"].as<String>().length() == 0 || root["hostname"].as<String>().length() > WIFI_MAX_HOSTNAME_STRLEN) {
|
||||
retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
if (NetworkSettings.NetworkMode() == network_mode::WiFi) {
|
||||
if (root[F("ssid")].as<String>().length() == 0 || root[F("ssid")].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
|
||||
retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!");
|
||||
if (root["ssid"].as<String>().length() == 0 || root["ssid"].as<String>().length() > WIFI_MAX_SSID_STRLEN) {
|
||||
retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (root[F("password")].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
|
||||
retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!");
|
||||
if (root["password"].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN - 1) {
|
||||
retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -209,19 +209,19 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
|
||||
config.WiFi_Dns2[1] = dns2[1];
|
||||
config.WiFi_Dns2[2] = dns2[2];
|
||||
config.WiFi_Dns2[3] = dns2[3];
|
||||
strlcpy(config.WiFi_Ssid, root[F("ssid")].as<String>().c_str(), sizeof(config.WiFi_Ssid));
|
||||
strlcpy(config.WiFi_Password, root[F("password")].as<String>().c_str(), sizeof(config.WiFi_Password));
|
||||
strlcpy(config.WiFi_Hostname, root[F("hostname")].as<String>().c_str(), sizeof(config.WiFi_Hostname));
|
||||
if (root[F("dhcp")].as<bool>()) {
|
||||
strlcpy(config.WiFi_Ssid, root["ssid"].as<String>().c_str(), sizeof(config.WiFi_Ssid));
|
||||
strlcpy(config.WiFi_Password, root["password"].as<String>().c_str(), sizeof(config.WiFi_Password));
|
||||
strlcpy(config.WiFi_Hostname, root["hostname"].as<String>().c_str(), sizeof(config.WiFi_Hostname));
|
||||
if (root["dhcp"].as<bool>()) {
|
||||
config.WiFi_Dhcp = true;
|
||||
} else {
|
||||
config.WiFi_Dhcp = false;
|
||||
}
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -38,29 +38,29 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("ntp_server")] = config.Ntp_Server;
|
||||
root[F("ntp_timezone")] = config.Ntp_Timezone;
|
||||
root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr;
|
||||
root["ntp_server"] = config.Ntp_Server;
|
||||
root["ntp_timezone"] = config.Ntp_Timezone;
|
||||
root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr;
|
||||
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
root[F("ntp_status")] = false;
|
||||
root["ntp_status"] = false;
|
||||
} else {
|
||||
root[F("ntp_status")] = true;
|
||||
root["ntp_status"] = true;
|
||||
}
|
||||
char timeStringBuff[50];
|
||||
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
|
||||
root[F("ntp_localtime")] = timeStringBuff;
|
||||
root["ntp_localtime"] = timeStringBuff;
|
||||
|
||||
SunPosition.sunriseTime(&timeinfo);
|
||||
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
|
||||
root[F("sun_risetime")] = timeStringBuff;
|
||||
root["sun_risetime"] = timeStringBuff;
|
||||
|
||||
SunPosition.sunsetTime(&timeinfo);
|
||||
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
|
||||
root[F("sun_settime")] = timeStringBuff;
|
||||
root["sun_settime"] = timeStringBuff;
|
||||
|
||||
root[F("sun_isDayPeriod")] = SunPosition.isDayPeriod();
|
||||
root["sun_isDayPeriod"] = SunPosition.isDayPeriod();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -76,11 +76,11 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("ntp_server")] = config.Ntp_Server;
|
||||
root[F("ntp_timezone")] = config.Ntp_Timezone;
|
||||
root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr;
|
||||
root[F("longitude")] = config.Ntp_Longitude;
|
||||
root[F("latitude")] = config.Ntp_Latitude;
|
||||
root["ntp_server"] = config.Ntp_Server;
|
||||
root["ntp_timezone"] = config.Ntp_Timezone;
|
||||
root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr;
|
||||
root["longitude"] = config.Ntp_Longitude;
|
||||
root["latitude"] = config.Ntp_Latitude;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -94,11 +94,11 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -107,8 +107,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -118,59 +118,59 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") && root.containsKey("latitude"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("ntp_server")].as<String>().length() == 0 || root[F("ntp_server")].as<String>().length() > NTP_MAX_SERVER_STRLEN) {
|
||||
retMsg[F("message")] = F("NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::NtpServerLength;
|
||||
retMsg[F("param")][F("max")] = NTP_MAX_SERVER_STRLEN;
|
||||
if (root["ntp_server"].as<String>().length() == 0 || root["ntp_server"].as<String>().length() > NTP_MAX_SERVER_STRLEN) {
|
||||
retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpServerLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("ntp_timezone")].as<String>().length() == 0 || root[F("ntp_timezone")].as<String>().length() > NTP_MAX_TIMEZONE_STRLEN) {
|
||||
retMsg[F("message")] = F("Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::NtpTimezoneLength;
|
||||
retMsg[F("param")][F("max")] = NTP_MAX_TIMEZONE_STRLEN;
|
||||
if (root["ntp_timezone"].as<String>().length() == 0 || root["ntp_timezone"].as<String>().length() > NTP_MAX_TIMEZONE_STRLEN) {
|
||||
retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpTimezoneLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("ntp_timezone_descr")].as<String>().length() == 0 || root[F("ntp_timezone_descr")].as<String>().length() > NTP_MAX_TIMEZONEDESCR_STRLEN) {
|
||||
retMsg[F("message")] = F("Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::NtpTimezoneDescriptionLength;
|
||||
retMsg[F("param")][F("max")] = NTP_MAX_TIMEZONEDESCR_STRLEN;
|
||||
if (root["ntp_timezone_descr"].as<String>().length() == 0 || root["ntp_timezone_descr"].as<String>().length() > NTP_MAX_TIMEZONEDESCR_STRLEN) {
|
||||
retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength;
|
||||
retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
strlcpy(config.Ntp_Server, root[F("ntp_server")].as<String>().c_str(), sizeof(config.Ntp_Server));
|
||||
strlcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as<String>().c_str(), sizeof(config.Ntp_Timezone));
|
||||
strlcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as<String>().c_str(), sizeof(config.Ntp_TimezoneDescr));
|
||||
config.Ntp_Latitude = root[F("latitude")].as<double>();
|
||||
config.Ntp_Longitude = root[F("longitude")].as<double>();
|
||||
strlcpy(config.Ntp_Server, root["ntp_server"].as<String>().c_str(), sizeof(config.Ntp_Server));
|
||||
strlcpy(config.Ntp_Timezone, root["ntp_timezone"].as<String>().c_str(), sizeof(config.Ntp_Timezone));
|
||||
strlcpy(config.Ntp_TimezoneDescr, root["ntp_timezone_descr"].as<String>().c_str(), sizeof(config.Ntp_TimezoneDescr));
|
||||
config.Ntp_Latitude = root["latitude"].as<double>();
|
||||
config.Ntp_Longitude = root["longitude"].as<double>();
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -190,17 +190,17 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request)
|
||||
|
||||
struct tm timeinfo;
|
||||
if (!getLocalTime(&timeinfo, 5)) {
|
||||
root[F("ntp_status")] = false;
|
||||
root["ntp_status"] = false;
|
||||
} else {
|
||||
root[F("ntp_status")] = true;
|
||||
root["ntp_status"] = true;
|
||||
}
|
||||
|
||||
root[F("year")] = timeinfo.tm_year + 1900;
|
||||
root[F("month")] = timeinfo.tm_mon + 1;
|
||||
root[F("day")] = timeinfo.tm_mday;
|
||||
root[F("hour")] = timeinfo.tm_hour;
|
||||
root[F("minute")] = timeinfo.tm_min;
|
||||
root[F("second")] = timeinfo.tm_sec;
|
||||
root["year"] = timeinfo.tm_year + 1900;
|
||||
root["month"] = timeinfo.tm_mon + 1;
|
||||
root["day"] = timeinfo.tm_mday;
|
||||
root["hour"] = timeinfo.tm_hour;
|
||||
root["minute"] = timeinfo.tm_min;
|
||||
root["second"] = timeinfo.tm_sec;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -214,11 +214,11 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -227,8 +227,8 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -238,8 +238,8 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -251,89 +251,89 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request)
|
||||
&& root.containsKey("hour")
|
||||
&& root.containsKey("minute")
|
||||
&& root.containsKey("second"))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("year")].as<uint>() < 2022 || root[F("year")].as<uint>() > 2100) {
|
||||
retMsg[F("message")] = F("Year must be a number between 2022 and 2100!");
|
||||
retMsg[F("code")] = WebApiError::NtpYearInvalid;
|
||||
retMsg[F("param")][F("min")] = 2022;
|
||||
retMsg[F("param")][F("max")] = 2100;
|
||||
if (root["year"].as<uint>() < 2022 || root["year"].as<uint>() > 2100) {
|
||||
retMsg["message"] = "Year must be a number between 2022 and 2100!";
|
||||
retMsg["code"] = WebApiError::NtpYearInvalid;
|
||||
retMsg["param"]["min"] = 2022;
|
||||
retMsg["param"]["max"] = 2100;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("month")].as<uint>() < 1 || root[F("month")].as<uint>() > 12) {
|
||||
retMsg[F("message")] = F("Month must be a number between 1 and 12!");
|
||||
retMsg[F("code")] = WebApiError::NtpMonthInvalid;
|
||||
retMsg[F("param")][F("min")] = 1;
|
||||
retMsg[F("param")][F("max")] = 12;
|
||||
if (root["month"].as<uint>() < 1 || root["month"].as<uint>() > 12) {
|
||||
retMsg["message"] = "Month must be a number between 1 and 12!";
|
||||
retMsg["code"] = WebApiError::NtpMonthInvalid;
|
||||
retMsg["param"]["min"] = 1;
|
||||
retMsg["param"]["max"] = 12;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("day")].as<uint>() < 1 || root[F("day")].as<uint>() > 31) {
|
||||
retMsg[F("message")] = F("Day must be a number between 1 and 31!");
|
||||
retMsg[F("code")] = WebApiError::NtpDayInvalid;
|
||||
retMsg[F("param")][F("min")] = 1;
|
||||
retMsg[F("param")][F("max")] = 31;
|
||||
if (root["day"].as<uint>() < 1 || root["day"].as<uint>() > 31) {
|
||||
retMsg["message"] = "Day must be a number between 1 and 31!";
|
||||
retMsg["code"] = WebApiError::NtpDayInvalid;
|
||||
retMsg["param"]["min"] = 1;
|
||||
retMsg["param"]["max"] = 31;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("hour")].as<uint>() > 23) {
|
||||
retMsg[F("message")] = F("Hour must be a number between 0 and 23!");
|
||||
retMsg[F("code")] = WebApiError::NtpHourInvalid;
|
||||
retMsg[F("param")][F("min")] = 0;
|
||||
retMsg[F("param")][F("max")] = 23;
|
||||
if (root["hour"].as<uint>() > 23) {
|
||||
retMsg["message"] = "Hour must be a number between 0 and 23!";
|
||||
retMsg["code"] = WebApiError::NtpHourInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 23;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("minute")].as<uint>() > 59) {
|
||||
retMsg[F("message")] = F("Minute must be a number between 0 and 59!");
|
||||
retMsg[F("code")] = WebApiError::NtpMinuteInvalid;
|
||||
retMsg[F("param")][F("min")] = 0;
|
||||
retMsg[F("param")][F("max")] = 59;
|
||||
if (root["minute"].as<uint>() > 59) {
|
||||
retMsg["message"] = "Minute must be a number between 0 and 59!";
|
||||
retMsg["code"] = WebApiError::NtpMinuteInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 59;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("second")].as<uint>() > 59) {
|
||||
retMsg[F("message")] = F("Second must be a number between 0 and 59!");
|
||||
retMsg[F("code")] = WebApiError::NtpSecondInvalid;
|
||||
retMsg[F("param")][F("min")] = 0;
|
||||
retMsg[F("param")][F("max")] = 59;
|
||||
if (root["second"].as<uint>() > 59) {
|
||||
retMsg["message"] = "Second must be a number between 0 and 59!";
|
||||
retMsg["code"] = WebApiError::NtpSecondInvalid;
|
||||
retMsg["param"]["min"] = 0;
|
||||
retMsg["param"]["max"] = 59;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
tm local;
|
||||
local.tm_sec = root[F("second")].as<uint>(); // seconds after the minute - [ 0 to 59 ]
|
||||
local.tm_min = root[F("minute")].as<uint>(); // minutes after the hour - [ 0 to 59 ]
|
||||
local.tm_hour = root[F("hour")].as<uint>(); // hours since midnight - [ 0 to 23 ]
|
||||
local.tm_mday = root[F("day")].as<uint>(); // day of the month - [ 1 to 31 ]
|
||||
local.tm_mon = root[F("month")].as<uint>() - 1; // months since January - [ 0 to 11 ]
|
||||
local.tm_year = root[F("year")].as<uint>() - 1900; // years since 1900
|
||||
local.tm_sec = root["second"].as<uint>(); // seconds after the minute - [ 0 to 59 ]
|
||||
local.tm_min = root["minute"].as<uint>(); // minutes after the hour - [ 0 to 59 ]
|
||||
local.tm_hour = root["hour"].as<uint>(); // hours since midnight - [ 0 to 23 ]
|
||||
local.tm_mday = root["day"].as<uint>(); // day of the month - [ 1 to 31 ]
|
||||
local.tm_mon = root["month"].as<uint>() - 1; // months since January - [ 0 to 11 ]
|
||||
local.tm_year = root["year"].as<uint>() - 1900; // years since 1900
|
||||
local.tm_isdst = -1;
|
||||
|
||||
time_t t = mktime(&local);
|
||||
struct timeval now = { .tv_sec = t, .tv_usec = 0 };
|
||||
settimeofday(&now, NULL);
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Time updated!");
|
||||
retMsg[F("code")] = WebApiError::NtpTimeUpdated;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Time updated!";
|
||||
retMsg["code"] = WebApiError::NtpTimeUpdated;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -58,11 +58,11 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -71,8 +71,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -82,8 +82,8 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -91,43 +91,43 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request)
|
||||
|
||||
if (!(root.containsKey("serial")
|
||||
&& (root.containsKey("power") || root.containsKey("restart")))) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("serial")].as<uint64_t>() == 0) {
|
||||
retMsg[F("message")] = F("Serial must be a number > 0!");
|
||||
retMsg[F("code")] = WebApiError::PowerSerialZero;
|
||||
if (root["serial"].as<uint64_t>() == 0) {
|
||||
retMsg["message"] = "Serial must be a number > 0!";
|
||||
retMsg["code"] = WebApiError::PowerSerialZero;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t serial = strtoll(root[F("serial")].as<String>().c_str(), NULL, 16);
|
||||
uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
|
||||
auto inv = Hoymiles.getInverterBySerial(serial);
|
||||
if (inv == nullptr) {
|
||||
retMsg[F("message")] = F("Invalid inverter specified!");
|
||||
retMsg[F("code")] = WebApiError::PowerInvalidInverter;
|
||||
retMsg["message"] = "Invalid inverter specified!";
|
||||
retMsg["code"] = WebApiError::PowerInvalidInverter;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root.containsKey("power")) {
|
||||
uint16_t power = root[F("power")].as<bool>();
|
||||
uint16_t power = root["power"].as<bool>();
|
||||
inv->sendPowerControlRequest(Hoymiles.getRadio(), power);
|
||||
} else {
|
||||
if (root[F("restart")].as<bool>()) {
|
||||
if (root["restart"].as<bool>()) {
|
||||
inv->sendRestartControlRequest(Hoymiles.getRadio());
|
||||
}
|
||||
}
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#include "MqttSettings.h"
|
||||
#include "PowerLimiter.h"
|
||||
#include "PowerMeter.h"
|
||||
#include "HttpPowerMeter.h"
|
||||
#include "WebApi.h"
|
||||
#include "helper.h"
|
||||
|
||||
@ -24,6 +25,7 @@ void WebApiPowerMeterClass::init(AsyncWebServer* server)
|
||||
_server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1));
|
||||
_server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1));
|
||||
_server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1));
|
||||
_server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1));
|
||||
}
|
||||
|
||||
void WebApiPowerMeterClass::loop()
|
||||
@ -44,6 +46,21 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
|
||||
root[F("mqtt_topic_powermeter_3")] = config.PowerMeter_MqttTopicPowerMeter3;
|
||||
root[F("sdmbaudrate")] = config.PowerMeter_SdmBaudrate;
|
||||
root[F("sdmaddress")] = config.PowerMeter_SdmAddress;
|
||||
root[F("http_individual_requests")] = config.PowerMeter_HttpIndividualRequests;
|
||||
|
||||
JsonArray httpPhases = root.createNestedArray(F("http_phases"));
|
||||
|
||||
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
||||
JsonObject phaseObject = httpPhases.createNestedObject();
|
||||
|
||||
phaseObject[F("index")] = i + 1;
|
||||
phaseObject[F("enabled")] = config.Powermeter_Http_Phase[i].Enabled;
|
||||
phaseObject[F("url")] = String(config.Powermeter_Http_Phase[i].Url);
|
||||
phaseObject[F("header_key")] = String(config.Powermeter_Http_Phase[i].HeaderKey);
|
||||
phaseObject[F("header_value")] = String(config.Powermeter_Http_Phase[i].HeaderValue);
|
||||
phaseObject[F("json_path")] = String(config.Powermeter_Http_Phase[i].JsonPath);
|
||||
phaseObject[F("timeout")] = config.Powermeter_Http_Phase[i].Timeout;
|
||||
}
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -77,14 +94,14 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
if (json.length() > 4096) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(1024);
|
||||
DynamicJsonDocument root(4096);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
@ -101,6 +118,44 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("source")].as<uint8_t>() == PowerMeter.SOURCE_HTTP) {
|
||||
JsonArray http_phases = root[F("http_phases")];
|
||||
for (uint8_t i = 0; i < http_phases.size(); i++) {
|
||||
JsonObject phase = http_phases[i].as<JsonObject>();
|
||||
|
||||
if (i > 0 && !phase[F("enabled")].as<bool>()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0 || phase[F("http_individual_requests")].as<bool>()) {
|
||||
if (!phase.containsKey("url")
|
||||
|| (!phase[F("url")].as<String>().startsWith("http://")
|
||||
&& !phase[F("url")].as<String>().startsWith("https://"))) {
|
||||
retMsg[F("message")] = F("URL must either start with http:// or https://!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!phase.containsKey("timeout")
|
||||
|| phase[F("timeout")].as<uint16_t>() <= 0) {
|
||||
retMsg[F("message")] = F("Timeout must be greater than 0 ms!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!phase.containsKey("json_path")
|
||||
|| phase[F("json_path")].as<String>().length() == 0) {
|
||||
retMsg[F("message")] = F("Json path must not be empty!");
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
config.PowerMeter_Enabled = root[F("enabled")].as<bool>();
|
||||
config.PowerMeter_Source = root[F("source")].as<uint8_t>();
|
||||
@ -110,6 +165,20 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
strlcpy(config.PowerMeter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as<String>().c_str(), sizeof(config.PowerMeter_MqttTopicPowerMeter3));
|
||||
config.PowerMeter_SdmBaudrate = root[F("sdmbaudrate")].as<uint32_t>();
|
||||
config.PowerMeter_SdmAddress = root[F("sdmaddress")].as<uint8_t>();
|
||||
config.PowerMeter_HttpIndividualRequests = root[F("http_individual_requests")].as<bool>();
|
||||
|
||||
JsonArray http_phases = root[F("http_phases")];
|
||||
for (uint8_t i = 0; i < http_phases.size(); i++) {
|
||||
JsonObject phase = http_phases[i].as<JsonObject>();
|
||||
|
||||
config.Powermeter_Http_Phase[i].Enabled = (i == 0 ? true : phase[F("enabled")].as<bool>());
|
||||
strlcpy(config.Powermeter_Http_Phase[i].Url, phase[F("url")].as<String>().c_str(), sizeof(config.Powermeter_Http_Phase[i].Url));
|
||||
strlcpy(config.Powermeter_Http_Phase[i].HeaderKey, phase[F("header_key")].as<String>().c_str(), sizeof(config.Powermeter_Http_Phase[i].HeaderKey));
|
||||
strlcpy(config.Powermeter_Http_Phase[i].HeaderValue, phase[F("header_value")].as<String>().c_str(), sizeof(config.Powermeter_Http_Phase[i].HeaderValue));
|
||||
config.Powermeter_Http_Phase[i].Timeout = phase[F("timeout")].as<uint16_t>();
|
||||
strlcpy(config.Powermeter_Http_Phase[i].JsonPath, phase[F("json_path")].as<String>().c_str(), sizeof(config.Powermeter_Http_Phase[i].JsonPath));
|
||||
}
|
||||
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
@ -123,3 +192,72 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
|
||||
yield();
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentials(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse();
|
||||
JsonObject retMsg = asyncJsonResponse->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
asyncJsonResponse->setLength();
|
||||
request->send(asyncJsonResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 2048) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
asyncJsonResponse->setLength();
|
||||
request->send(asyncJsonResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicJsonDocument root(2048);
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
asyncJsonResponse->setLength();
|
||||
request->send(asyncJsonResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.containsKey("url") || !root.containsKey("header_key") || !root.containsKey("header_value")
|
||||
|| !root.containsKey("timeout") || !root.containsKey("json_path")) {
|
||||
retMsg[F("message")] = F("Missing fields!");
|
||||
asyncJsonResponse->setLength();
|
||||
request->send(asyncJsonResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
char powerMeterResponse[2000],
|
||||
errorMessage[256];
|
||||
char response[200];
|
||||
|
||||
if (HttpPowerMeter.httpRequest(root[F("url")].as<String>().c_str(), root[F("header_key")].as<String>().c_str(),
|
||||
root[F("header_value")].as<String>().c_str(), root[F("timeout")].as<uint16_t>(),
|
||||
powerMeterResponse, sizeof(powerMeterResponse), errorMessage, sizeof(errorMessage))) {
|
||||
float power;
|
||||
|
||||
if (HttpPowerMeter.getFloatValueByJsonPath(powerMeterResponse,
|
||||
root[F("json_path")].as<String>().c_str(), power)) {
|
||||
retMsg[F("type")] = F("success");
|
||||
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", power);
|
||||
} else {
|
||||
snprintf_P(response, sizeof(response), "Error: Could not find value for JSON path!");
|
||||
}
|
||||
} else {
|
||||
snprintf_P(response, sizeof(response), errorMessage);
|
||||
}
|
||||
|
||||
retMsg[F("message")] = F(response);
|
||||
asyncJsonResponse->setLength();
|
||||
request->send(asyncJsonResponse);
|
||||
}
|
||||
|
||||
@ -29,29 +29,29 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
|
||||
try {
|
||||
auto stream = request->beginResponseStream("text/plain; charset=utf-8", 40960);
|
||||
|
||||
stream->print(F("# HELP opendtu_build Build info\n"));
|
||||
stream->print(F("# TYPE opendtu_build gauge\n"));
|
||||
stream->print("# HELP opendtu_build Build info\n");
|
||||
stream->print("# TYPE opendtu_build gauge\n");
|
||||
stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n",
|
||||
NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
|
||||
|
||||
stream->print(F("# HELP opendtu_platform Platform info\n"));
|
||||
stream->print(F("# TYPE opendtu_platform gauge\n"));
|
||||
stream->print("# HELP opendtu_platform Platform info\n");
|
||||
stream->print("# TYPE opendtu_platform gauge\n");
|
||||
stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), NetworkSettings.macAddress().c_str());
|
||||
|
||||
stream->print(F("# HELP opendtu_uptime Uptime in seconds\n"));
|
||||
stream->print(F("# TYPE opendtu_uptime counter\n"));
|
||||
stream->print("# HELP opendtu_uptime Uptime in seconds\n");
|
||||
stream->print("# TYPE opendtu_uptime counter\n");
|
||||
stream->printf("opendtu_uptime %lld\n", esp_timer_get_time() / 1000000);
|
||||
|
||||
stream->print(F("# HELP opendtu_heap_size System memory size\n"));
|
||||
stream->print(F("# TYPE opendtu_heap_size gauge\n"));
|
||||
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->print(F("# HELP opendtu_free_heap_size System free memory\n"));
|
||||
stream->print(F("# TYPE opendtu_free_heap_size gauge\n"));
|
||||
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->print(F("# HELP wifi_rssi WiFi RSSI\n"));
|
||||
stream->print(F("# TYPE wifi_rssi gauge\n"));
|
||||
stream->print("# HELP wifi_rssi WiFi RSSI\n");
|
||||
stream->print("# TYPE wifi_rssi gauge\n");
|
||||
stream->printf("wifi_rssi %d\n", WiFi.RSSI());
|
||||
|
||||
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
|
||||
@ -60,8 +60,8 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
|
||||
String serial = inv->serialString();
|
||||
const char* name = inv->name();
|
||||
if (i == 0) {
|
||||
stream->print(F("# HELP opendtu_last_update last update from inverter in s\n"));
|
||||
stream->print(F("# TYPE opendtu_last_update gauge\n"));
|
||||
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",
|
||||
serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000);
|
||||
@ -93,7 +93,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
|
||||
}
|
||||
}
|
||||
}
|
||||
stream->addHeader(F("Cache-Control"), F("no-cache"));
|
||||
stream->addHeader("Cache-Control", "no-cache");
|
||||
request->send(stream);
|
||||
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
@ -132,8 +132,8 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se
|
||||
|
||||
const bool printHelp = (idx == 0 && channel == 0);
|
||||
if (printHelp) {
|
||||
stream->print(F("# HELP opendtu_PanelInfo panel information\n"));
|
||||
stream->print(F("# TYPE opendtu_PanelInfo gauge\n"));
|
||||
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",
|
||||
serial.c_str(),
|
||||
@ -144,8 +144,8 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se
|
||||
);
|
||||
|
||||
if (printHelp) {
|
||||
stream->print(F("# HELP opendtu_MaxPower panel maximum output power\n"));
|
||||
stream->print(F("# TYPE opendtu_MaxPower gauge\n"));
|
||||
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",
|
||||
serial.c_str(),
|
||||
@ -156,8 +156,8 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& se
|
||||
);
|
||||
|
||||
if (printHelp) {
|
||||
stream->print(F("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"));
|
||||
stream->print(F("# TYPE opendtu_YieldTotalOffset gauge\n"));
|
||||
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",
|
||||
serial.c_str(),
|
||||
|
||||
@ -34,8 +34,8 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request)
|
||||
JsonObject root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
|
||||
root[F("password")] = config.Security_Password;
|
||||
root[F("allow_readonly")] = config.Security_AllowReadonly;
|
||||
root["password"] = config.Security_Password;
|
||||
root["allow_readonly"] = config.Security_AllowReadonly;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -49,11 +49,11 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("warning");
|
||||
retMsg["type"] = "warning";
|
||||
|
||||
if (!request->hasParam("data", true)) {
|
||||
retMsg[F("message")] = F("No values found!");
|
||||
retMsg[F("code")] = WebApiError::GenericNoValueFound;
|
||||
retMsg["message"] = "No values found!";
|
||||
retMsg["code"] = WebApiError::GenericNoValueFound;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -62,8 +62,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
String json = request->getParam("data", true)->value();
|
||||
|
||||
if (json.length() > 1024) {
|
||||
retMsg[F("message")] = F("Data too large!");
|
||||
retMsg[F("code")] = WebApiError::GenericDataTooLarge;
|
||||
retMsg["message"] = "Data too large!";
|
||||
retMsg["code"] = WebApiError::GenericDataTooLarge;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -73,8 +73,8 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
DeserializationError error = deserializeJson(root, json);
|
||||
|
||||
if (error) {
|
||||
retMsg[F("message")] = F("Failed to parse data!");
|
||||
retMsg[F("code")] = WebApiError::GenericParseError;
|
||||
retMsg["message"] = "Failed to parse data!";
|
||||
retMsg["code"] = WebApiError::GenericParseError;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
@ -82,30 +82,30 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
|
||||
|
||||
if (!root.containsKey("password")
|
||||
&& root.containsKey("allow_readonly")) {
|
||||
retMsg[F("message")] = F("Values are missing!");
|
||||
retMsg[F("code")] = WebApiError::GenericValueMissing;
|
||||
retMsg["message"] = "Values are missing!";
|
||||
retMsg["code"] = WebApiError::GenericValueMissing;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (root[F("password")].as<String>().length() < 8 || root[F("password")].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN) {
|
||||
retMsg[F("message")] = F("Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!");
|
||||
retMsg[F("code")] = WebApiError::SecurityPasswordLength;
|
||||
retMsg[F("param")][F("max")] = WIFI_MAX_PASSWORD_STRLEN;
|
||||
if (root["password"].as<String>().length() < 8 || root["password"].as<String>().length() > WIFI_MAX_PASSWORD_STRLEN) {
|
||||
retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!";
|
||||
retMsg["code"] = WebApiError::SecurityPasswordLength;
|
||||
retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
return;
|
||||
}
|
||||
|
||||
CONFIG_T& config = Configuration.get();
|
||||
strlcpy(config.Security_Password, root[F("password")].as<String>().c_str(), sizeof(config.Security_Password));
|
||||
config.Security_AllowReadonly = root[F("allow_readonly")].as<bool>();
|
||||
strlcpy(config.Security_Password, root["password"].as<String>().c_str(), sizeof(config.Security_Password));
|
||||
config.Security_AllowReadonly = root["allow_readonly"].as<bool>();
|
||||
Configuration.write();
|
||||
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Settings saved!");
|
||||
retMsg[F("code")] = WebApiError::GenericSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Settings saved!";
|
||||
retMsg["code"] = WebApiError::GenericSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
@ -119,9 +119,9 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject retMsg = response->getRoot();
|
||||
retMsg[F("type")] = F("success");
|
||||
retMsg[F("message")] = F("Authentication successful!");
|
||||
retMsg[F("code")] = WebApiError::SecurityAuthSuccess;
|
||||
retMsg["type"] = "success";
|
||||
retMsg["message"] = "Authentication successful!";
|
||||
retMsg["code"] = WebApiError::SecurityAuthSuccess;
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
@ -37,40 +37,40 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot();
|
||||
|
||||
root[F("hostname")] = NetworkSettings.getHostname();
|
||||
root["hostname"] = NetworkSettings.getHostname();
|
||||
|
||||
root[F("sdkversion")] = ESP.getSdkVersion();
|
||||
root[F("cpufreq")] = ESP.getCpuFreqMHz();
|
||||
root["sdkversion"] = ESP.getSdkVersion();
|
||||
root["cpufreq"] = ESP.getCpuFreqMHz();
|
||||
|
||||
root[F("heap_total")] = ESP.getHeapSize();
|
||||
root[F("heap_used")] = ESP.getHeapSize() - ESP.getFreeHeap();
|
||||
root[F("sketch_total")] = ESP.getFreeSketchSpace();
|
||||
root[F("sketch_used")] = ESP.getSketchSize();
|
||||
root[F("littlefs_total")] = LittleFS.totalBytes();
|
||||
root[F("littlefs_used")] = LittleFS.usedBytes();
|
||||
root["heap_total"] = ESP.getHeapSize();
|
||||
root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap();
|
||||
root["sketch_total"] = ESP.getFreeSketchSpace();
|
||||
root["sketch_used"] = ESP.getSketchSize();
|
||||
root["littlefs_total"] = LittleFS.totalBytes();
|
||||
root["littlefs_used"] = LittleFS.usedBytes();
|
||||
|
||||
root[F("chiprevision")] = ESP.getChipRevision();
|
||||
root[F("chipmodel")] = ESP.getChipModel();
|
||||
root[F("chipcores")] = ESP.getChipCores();
|
||||
root["chiprevision"] = ESP.getChipRevision();
|
||||
root["chipmodel"] = ESP.getChipModel();
|
||||
root["chipcores"] = ESP.getChipCores();
|
||||
|
||||
String reason;
|
||||
reason = ResetReason.get_reset_reason_verbose(0);
|
||||
root[F("resetreason_0")] = reason;
|
||||
root["resetreason_0"] = reason;
|
||||
|
||||
reason = ResetReason.get_reset_reason_verbose(1);
|
||||
root[F("resetreason_1")] = reason;
|
||||
root["resetreason_1"] = reason;
|
||||
|
||||
root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount;
|
||||
root["cfgsavecount"] = Configuration.get().Cfg_SaveCount;
|
||||
|
||||
char version[16];
|
||||
snprintf(version, sizeof(version), "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff);
|
||||
root[F("config_version")] = version;
|
||||
root[F("git_hash")] = AUTO_GIT_HASH;
|
||||
root["config_version"] = version;
|
||||
root["git_hash"] = AUTO_GIT_HASH;
|
||||
|
||||
root[F("uptime")] = esp_timer_get_time() / 1000000;
|
||||
root["uptime"] = esp_timer_get_time() / 1000000;
|
||||
|
||||
root[F("radio_connected")] = Hoymiles.getRadio()->isConnected();
|
||||
root[F("radio_pvariant")] = Hoymiles.getRadio()->isPVariant();
|
||||
root["radio_connected"] = Hoymiles.getRadio()->isConnected();
|
||||
root["radio_pvariant"] = Hoymiles.getRadio()->isPVariant();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
136
src/WebApi_ws_Huawei.cpp
Normal file
136
src/WebApi_ws_Huawei.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_Huawei.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass()
|
||||
: _ws("/huaweilivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
_server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsHuaweiLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::loop()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
if (millis() - _lastWsCleanup > 1000) {
|
||||
_ws.cleanupClients();
|
||||
_lastWsCleanup = millis();
|
||||
}
|
||||
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - _lastUpdateCheck < 1000) {
|
||||
return;
|
||||
}
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
String buffer;
|
||||
// free JsonDocument as soon as possible
|
||||
{
|
||||
DynamicJsonDocument root(1024);
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
serializeJson(root, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
{
|
||||
const RectifierParameters_t * rp = HuaweiCan.get();
|
||||
|
||||
root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000;
|
||||
root[F("input_voltage")]["v"] = rp->input_voltage;
|
||||
root[F("input_voltage")]["u"] = "V";
|
||||
root[F("input_current")]["v"] = rp->input_current;
|
||||
root[F("input_current")]["u"] = "A";
|
||||
root[F("input_power")]["v"] = rp->input_power;
|
||||
root[F("input_power")]["u"] = "W";
|
||||
root[F("output_voltage")]["v"] = rp->output_voltage;
|
||||
root[F("output_voltage")]["u"] = "V";
|
||||
root[F("output_current")]["v"] = rp->output_current;
|
||||
root[F("output_current")]["u"] = "A";
|
||||
root[F("max_output_current")]["v"] = rp->max_output_current;
|
||||
root[F("max_output_current")]["u"] = "A";
|
||||
root[F("output_power")]["v"] = rp->output_power;
|
||||
root[F("output_power")]["u"] = "W";
|
||||
root[F("input_temp")]["v"] = rp->input_temp;
|
||||
root[F("input_temp")]["u"] = "°C";
|
||||
root[F("output_temp")]["v"] = rp->output_temp;
|
||||
root[F("output_temp")]["u"] = "°C";
|
||||
root[F("efficiency")]["v"] = rp->efficiency;
|
||||
root[F("efficiency")]["u"] = "%";
|
||||
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
|
||||
JsonVariant root = response->getRoot().as<JsonVariant>();
|
||||
generateJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
156
src/WebApi_ws_Pylontech.cpp
Normal file
156
src/WebApi_ws_Pylontech.cpp
Normal file
@ -0,0 +1,156 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
* Copyright (C) 2022 Thomas Basler and others
|
||||
*/
|
||||
#include "WebApi_ws_Pylontech.h"
|
||||
#include "AsyncJson.h"
|
||||
#include "Configuration.h"
|
||||
#include "Battery.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "defaults.h"
|
||||
|
||||
WebApiWsPylontechLiveClass::WebApiWsPylontechLiveClass()
|
||||
: _ws("/batterylivedata")
|
||||
{
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::init(AsyncWebServer* server)
|
||||
{
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
using std::placeholders::_4;
|
||||
using std::placeholders::_5;
|
||||
using std::placeholders::_6;
|
||||
|
||||
_server = server;
|
||||
_server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsPylontechLiveClass::onLivedataStatus, this, _1));
|
||||
|
||||
_server->addHandler(&_ws);
|
||||
_ws.onEvent(std::bind(&WebApiWsPylontechLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6));
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::loop()
|
||||
{
|
||||
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
|
||||
if (millis() - _lastWsCleanup > 1000) {
|
||||
_ws.cleanupClients();
|
||||
_lastWsCleanup = millis();
|
||||
}
|
||||
|
||||
// do nothing if no WS client is connected
|
||||
if (_ws.count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - _lastUpdateCheck < 1000) {
|
||||
return;
|
||||
}
|
||||
_lastUpdateCheck = millis();
|
||||
|
||||
try {
|
||||
String buffer;
|
||||
// free JsonDocument as soon as possible
|
||||
{
|
||||
DynamicJsonDocument root(1024);
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
serializeJson(root, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
{
|
||||
root["data_age"] = (millis() - Battery.lastUpdate) / 1000;
|
||||
|
||||
root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ;
|
||||
root[F("chargeVoltage")]["u"] = "V";
|
||||
root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ;
|
||||
root[F("chargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ;
|
||||
root[F("dischargeCurrentLimitation")]["u"] = "A";
|
||||
root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ;
|
||||
root[F("stateOfCharge")]["u"] = "%";
|
||||
root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ;
|
||||
root[F("stateOfHealth")]["u"] = "%";
|
||||
root[F("voltage")]["v"] = Battery.voltage;
|
||||
root[F("voltage")]["u"] = "V";
|
||||
root[F("current")]["v"] = Battery.current ;
|
||||
root[F("current")]["u"] = "A";
|
||||
root[F("temperature")]["v"] = Battery.temperature ;
|
||||
root[F("temperature")]["u"] = "°C";
|
||||
|
||||
// Alarms
|
||||
root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ;
|
||||
root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ;
|
||||
root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ;
|
||||
root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ;
|
||||
root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ;
|
||||
root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ;
|
||||
root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ;
|
||||
|
||||
// Warnings
|
||||
root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ;
|
||||
root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ;
|
||||
root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ;
|
||||
root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ;
|
||||
root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ;
|
||||
root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ;
|
||||
root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ;
|
||||
|
||||
// Misc
|
||||
root[F("manufacturer")] = Battery.manufacturer ;
|
||||
root[F("chargeEnabled")] = Battery.chargeEnabled ;
|
||||
root[F("dischargeEnabled")] = Battery.dischargeEnabled ;
|
||||
root[F("chargeImmediately")] = Battery.chargeImmediately ;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
{
|
||||
if (type == WS_EVT_CONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
} else if (type == WS_EVT_DISCONNECT) {
|
||||
char str[64];
|
||||
snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id());
|
||||
Serial.println(str);
|
||||
MessageOutput.println(str);
|
||||
}
|
||||
}
|
||||
|
||||
void WebApiWsPylontechLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
{
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
|
||||
JsonVariant root = response->getRoot().as<JsonVariant>();
|
||||
generateJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@
|
||||
#include "Configuration.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "WebApi.h"
|
||||
#include "Battery.h"
|
||||
#include "VeDirectFrameHandler.h"
|
||||
#include "defaults.h"
|
||||
#include <AsyncJson.h>
|
||||
|
||||
@ -105,16 +107,17 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
|
||||
JsonObject invObject = invArray.createNestedObject();
|
||||
|
||||
invObject[F("serial")] = inv->serialString();
|
||||
invObject[F("name")] = inv->name();
|
||||
invObject[F("data_age")] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
|
||||
invObject[F("reachable")] = inv->isReachable();
|
||||
invObject[F("producing")] = inv->isProducing();
|
||||
invObject[F("limit_relative")] = inv->SystemConfigPara()->getLimitPercent();
|
||||
invObject["serial"] = inv->serialString();
|
||||
invObject["name"] = inv->name();
|
||||
invObject["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
|
||||
invObject["poll_enabled"] = inv->getEnablePolling();
|
||||
invObject["reachable"] = inv->isReachable();
|
||||
invObject["producing"] = inv->isProducing();
|
||||
invObject["limit_relative"] = inv->SystemConfigPara()->getLimitPercent();
|
||||
if (inv->DevInfo()->getMaxPower() > 0) {
|
||||
invObject[F("limit_absolute")] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
|
||||
invObject["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0;
|
||||
} else {
|
||||
invObject[F("limit_absolute")] = -1;
|
||||
invObject["limit_absolute"] = -1;
|
||||
}
|
||||
|
||||
// Loop all channels
|
||||
@ -124,14 +127,14 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
if (t == TYPE_DC) {
|
||||
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
|
||||
if (inv_cfg != nullptr) {
|
||||
chanTypeObj[String(static_cast<uint8_t>(c))][F("name")]["u"] = inv_cfg->channel[c].Name;
|
||||
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
|
||||
}
|
||||
}
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_PAC);
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_UAC);
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_IAC);
|
||||
if (t == TYPE_AC) {
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_PDC, F("Power DC"));
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_PDC, "Power DC");
|
||||
} else {
|
||||
addField(chanTypeObj, i, inv, t, c, FLD_PDC);
|
||||
}
|
||||
@ -151,9 +154,9 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
}
|
||||
|
||||
if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
|
||||
invObject[F("events")] = inv->EventLog()->getEntryCount();
|
||||
invObject["events"] = inv->EventLog()->getEntryCount();
|
||||
} else {
|
||||
invObject[F("events")] = -1;
|
||||
invObject["events"] = -1;
|
||||
}
|
||||
|
||||
if (inv->Statistics()->getLastUpdate() > _newestInverterTimestamp) {
|
||||
@ -175,16 +178,27 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
|
||||
|
||||
JsonObject hintObj = root.createNestedObject("hints");
|
||||
struct tm timeinfo;
|
||||
hintObj[F("time_sync")] = !getLocalTime(&timeinfo, 5);
|
||||
hintObj[F("radio_problem")] = (!Hoymiles.getRadio()->isConnected() || !Hoymiles.getRadio()->isPVariant());
|
||||
hintObj["time_sync"] = !getLocalTime(&timeinfo, 5);
|
||||
hintObj["radio_problem"] = (!Hoymiles.getRadio()->isConnected() || !Hoymiles.getRadio()->isPVariant());
|
||||
if (!strcmp(Configuration.get().Security_Password, ACCESS_POINT_PASSWORD)) {
|
||||
hintObj[F("default_password")] = true;
|
||||
hintObj["default_password"] = true;
|
||||
} else {
|
||||
hintObj[F("default_password")] = false;
|
||||
hintObj["default_password"] = false;
|
||||
}
|
||||
|
||||
JsonObject vedirectObj = root.createNestedObject("vedirect");
|
||||
vedirectObj[F("enabled")] = Configuration.get().Vedirect_Enabled;
|
||||
JsonObject totalVeObj = vedirectObj.createNestedObject("total");
|
||||
addTotalField(totalVeObj, "Power", VeDirect.veFrame.PPV, "W", 1);
|
||||
addTotalField(totalVeObj, "YieldDay", VeDirect.veFrame.H20 * 1000, "Wh", 0);
|
||||
addTotalField(totalVeObj, "YieldTotal", VeDirect.veFrame.H19, "kWh", 2);
|
||||
|
||||
JsonObject huaweiObj = root.createNestedObject("huawei");
|
||||
huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled;
|
||||
|
||||
JsonObject batteryObj = root.createNestedObject("battery");
|
||||
batteryObj[F("enabled")] = Configuration.get().Battery_Enabled;
|
||||
addTotalField(batteryObj, "soc", Battery.stateOfCharge, "%", 0);
|
||||
}
|
||||
|
||||
void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic)
|
||||
|
||||
@ -58,23 +58,25 @@ void WebApiWsVedirectLiveClass::loop()
|
||||
if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestVedirectTimestamp)) {
|
||||
|
||||
try {
|
||||
String buffer;
|
||||
// free JsonDocument as soon as possible
|
||||
{
|
||||
DynamicJsonDocument root(1024);
|
||||
JsonVariant var = root;
|
||||
generateJsonResponse(var);
|
||||
|
||||
String buffer;
|
||||
if (buffer) {
|
||||
serializeJson(root, buffer);
|
||||
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
serializeJson(root, buffer);
|
||||
}
|
||||
|
||||
if (buffer) {
|
||||
if (Configuration.get().Security_AllowReadonly) {
|
||||
_ws.setAuthentication("", "");
|
||||
} else {
|
||||
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password);
|
||||
}
|
||||
|
||||
_ws.textAll(buffer);
|
||||
}
|
||||
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/vedirectlivedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
}
|
||||
@ -153,10 +155,18 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request)
|
||||
if (!WebApi.checkCredentialsReadonly(request)) {
|
||||
return;
|
||||
}
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
|
||||
JsonVariant root = response->getRoot().as<JsonVariant>();
|
||||
generateJsonResponse(root);
|
||||
try {
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
|
||||
JsonVariant root = response->getRoot();
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
generateJsonResponse(root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
|
||||
} catch (std::bad_alloc& bad_alloc) {
|
||||
MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what());
|
||||
|
||||
WebApi.sendTooManyRequests(request);
|
||||
}
|
||||
}
|
||||
85
src/main.cpp
85
src/main.cpp
@ -5,14 +5,17 @@
|
||||
#include "Configuration.h"
|
||||
#include "Display_Graphic.h"
|
||||
#include "InverterSettings.h"
|
||||
#include "Led_Single.h"
|
||||
#include "MessageOutput.h"
|
||||
#include "VeDirectFrameHandler.h"
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "Huawei_can.h"
|
||||
#include "MqttHandleDtu.h"
|
||||
#include "MqttHandleHass.h"
|
||||
#include "MqttHandleVedirectHass.h"
|
||||
#include "MqttHandleInverter.h"
|
||||
#include "MqttHandleVedirect.h"
|
||||
#include "MqttHandleHuawei.h"
|
||||
#include "MqttSettings.h"
|
||||
#include "NetworkSettings.h"
|
||||
#include "NtpSettings.h"
|
||||
@ -33,82 +36,83 @@ void setup()
|
||||
while (!Serial)
|
||||
yield();
|
||||
MessageOutput.println();
|
||||
MessageOutput.println(F("Starting OpenDTU"));
|
||||
MessageOutput.println("Starting OpenDTU");
|
||||
|
||||
// Initialize file system
|
||||
MessageOutput.print(F("Initialize FS... "));
|
||||
MessageOutput.print("Initialize FS... ");
|
||||
if (!LittleFS.begin(false)) { // Do not format if mount failed
|
||||
MessageOutput.print(F("failed... trying to format..."));
|
||||
MessageOutput.print("failed... trying to format...");
|
||||
if (!LittleFS.begin(true)) {
|
||||
MessageOutput.print("success");
|
||||
} else {
|
||||
MessageOutput.print("failed");
|
||||
}
|
||||
} else {
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
}
|
||||
|
||||
// Read configuration values
|
||||
MessageOutput.print(F("Reading configuration... "));
|
||||
MessageOutput.print("Reading configuration... ");
|
||||
if (!Configuration.read()) {
|
||||
MessageOutput.print(F("initializing... "));
|
||||
MessageOutput.print("initializing... ");
|
||||
Configuration.init();
|
||||
if (Configuration.write()) {
|
||||
MessageOutput.print(F("written... "));
|
||||
MessageOutput.print("written... ");
|
||||
} else {
|
||||
MessageOutput.print(F("failed... "));
|
||||
MessageOutput.print("failed... ");
|
||||
}
|
||||
}
|
||||
if (Configuration.get().Cfg_Version != CONFIG_VERSION) {
|
||||
MessageOutput.print(F("migrated... "));
|
||||
MessageOutput.print("migrated... ");
|
||||
Configuration.migrate();
|
||||
}
|
||||
CONFIG_T& config = Configuration.get();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Load PinMapping
|
||||
MessageOutput.print(F("Reading PinMapping... "));
|
||||
MessageOutput.print("Reading PinMapping... ");
|
||||
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
|
||||
MessageOutput.print(F("found valid mapping "));
|
||||
MessageOutput.print("found valid mapping ");
|
||||
} else {
|
||||
MessageOutput.print(F("using default config "));
|
||||
MessageOutput.print("using default config ");
|
||||
}
|
||||
const PinMapping_t& pin = PinMapping.get();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize WiFi
|
||||
MessageOutput.print(F("Initialize Network... "));
|
||||
MessageOutput.print("Initialize Network... ");
|
||||
NetworkSettings.init();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
NetworkSettings.applyConfig();
|
||||
|
||||
// Initialize NTP
|
||||
MessageOutput.print(F("Initialize NTP... "));
|
||||
MessageOutput.print("Initialize NTP... ");
|
||||
NtpSettings.init();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize SunPosition
|
||||
MessageOutput.print(F("Initialize SunPosition... "));
|
||||
MessageOutput.print("Initialize SunPosition... ");
|
||||
SunPosition.init();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize MqTT
|
||||
MessageOutput.print(F("Initialize MqTT... "));
|
||||
MessageOutput.print("Initialize MqTT... ");
|
||||
MqttSettings.init();
|
||||
MqttHandleDtu.init();
|
||||
MqttHandleInverter.init();
|
||||
MqttHandleVedirect.init();
|
||||
MqttHandleHass.init();
|
||||
MqttHandleVedirectHass.init();
|
||||
MessageOutput.println(F("done"));
|
||||
MqttHandleHuawei.init();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize WebApi
|
||||
MessageOutput.print(F("Initialize WebApi... "));
|
||||
MessageOutput.print("Initialize WebApi... ");
|
||||
WebApi.init();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Display
|
||||
MessageOutput.print(F("Initialize Display... "));
|
||||
MessageOutput.print("Initialize Display... ");
|
||||
Display.init(
|
||||
static_cast<DisplayType_t>(pin.display_type),
|
||||
pin.display_data,
|
||||
@ -120,12 +124,17 @@ void setup()
|
||||
Display.enableScreensaver = config.Display_ScreenSaver;
|
||||
Display.setContrast(config.Display_Contrast);
|
||||
Display.setStartupDisplay();
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Initialize Single LEDs
|
||||
MessageOutput.print("Initialize LEDs... ");
|
||||
LedSingle.init();
|
||||
MessageOutput.println("done");
|
||||
|
||||
// Check for default DTU serial
|
||||
MessageOutput.print(F("Check for default DTU serial... "));
|
||||
MessageOutput.print("Check for default DTU serial... ");
|
||||
if (config.Dtu_Serial == DTU_SERIAL) {
|
||||
MessageOutput.print(F("generate serial based on ESP chip id: "));
|
||||
MessageOutput.print("generate serial based on ESP chip id: ");
|
||||
uint64_t dtuId = Utils::generateDtuSerial();
|
||||
MessageOutput.printf("%0x%08x... ",
|
||||
((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
|
||||
@ -133,7 +142,8 @@ void setup()
|
||||
config.Dtu_Serial = dtuId;
|
||||
Configuration.write();
|
||||
}
|
||||
MessageOutput.println(F("done"));
|
||||
MessageOutput.println("done");
|
||||
MessageOutput.println("done");
|
||||
|
||||
InverterSettings.init();
|
||||
|
||||
@ -162,6 +172,17 @@ void setup()
|
||||
} else {
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
}
|
||||
|
||||
// Initialize Huawei AC-charger PSU / CAN bus
|
||||
MessageOutput.println(F("Initialize Huawei AC charger interface... "));
|
||||
if (PinMapping.isValidHuaweiConfig()) {
|
||||
MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
HuaweiCan.init(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power);
|
||||
MessageOutput.println(F("done"));
|
||||
} else {
|
||||
MessageOutput.println(F("Invalid pin config"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void loop()
|
||||
@ -189,6 +210,8 @@ void loop()
|
||||
yield();
|
||||
MqttHandleVedirectHass.loop();
|
||||
yield();
|
||||
MqttHandleHuawei.loop();
|
||||
yield();
|
||||
WebApi.loop();
|
||||
yield();
|
||||
Display.loop();
|
||||
@ -199,4 +222,8 @@ void loop()
|
||||
yield();
|
||||
PylontechCanReceiver.loop();
|
||||
yield();
|
||||
HuaweiCan.loop();
|
||||
yield();
|
||||
LedSingle.loop();
|
||||
yield();
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"bootstrap": "^5.3.0-alpha2",
|
||||
"bootstrap": "^5.3.0-alpha3",
|
||||
"bootstrap-icons-vue": "^1.10.3",
|
||||
"mitt": "^3.0.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
@ -24,17 +24,17 @@
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@rushstack/eslint-patch": "^1.2.0",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/node": "^18.15.10",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.2",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-plugin-vue": "^9.10.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "^1.60.0",
|
||||
"terser": "^5.16.8",
|
||||
"typescript": "^5.0.2",
|
||||
"typescript": "^5.0.3",
|
||||
"vite": "^4.2.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-css-injected-by-js": "^3.1.0",
|
||||
|
||||
@ -2,7 +2,15 @@
|
||||
<div :class="{'container-xxl': !isWideScreen,
|
||||
'container-fluid': isWideScreen}" role="main">
|
||||
<div class="page-header">
|
||||
<h1>{{ title }}</h1>
|
||||
<div class="row">
|
||||
<div class="col-sm-11">
|
||||
<h1>{{ title }}</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center" v-if="isLoading">
|
||||
@ -19,12 +27,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { BIconArrowClockwise } from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconArrowClockwise,
|
||||
},
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
isLoading: { type: Boolean, required: false, default: false },
|
||||
isWideScreen: { type: Boolean, required: false, default: false },
|
||||
showReload: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
359
webapp/src/components/BatteryView.vue
Normal file
359
webapp/src/components/BatteryView.vue
Normal file
@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': batteryData.data_age > 20,
|
||||
'text-bg-primary': batteryData.data_age < 20,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.battery') }}: {{ batteryData.manufacturer }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('battery.DataAge') }} {{ $t('battery.Seconds', { 'val': batteryData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('battery.Status') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('battery.Value') }}</th>
|
||||
<th scope="col">{{ $t('battery.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.stateOfCharge') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.stateOfCharge.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.stateOfCharge.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.stateOfHealth') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.stateOfHealth.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.stateOfHealth.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.voltage') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.current') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.current.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.temperature') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.temperature.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.temperature.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeVoltage') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.chargeVoltage.v.toFixed(1) }}</td>
|
||||
<td>{{ batteryData.chargeVoltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeCurrentLimitation') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.chargeCurrentLimitation.v }}</td>
|
||||
<td>{{ batteryData.chargeCurrentLimitation.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.dischargeCurrentLimitation') }}</th>
|
||||
<td style="text-align: right">{{ batteryData.dischargeCurrentLimitation.v }}</td>
|
||||
<td>{{ batteryData.dischargeCurrentLimitation.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('battery.warn_alarm') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('battery.Property') }}</th>
|
||||
<th scope="col">{{ $t('battery.alarm') }}</th>
|
||||
<th scope="col">{{ $t('battery.warning') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.dischargeCurrent') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.dischargeCurrent,
|
||||
'text-bg-success': !batteryData.alarms.dischargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.dischargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.dischargeCurrent,
|
||||
'text-bg-success': !batteryData.warnings.dischargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.dischargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.chargeCurrent') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.chargeCurrent,
|
||||
'text-bg-success': !batteryData.alarms.chargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.chargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.chargeCurrent,
|
||||
'text-bg-success': !batteryData.warnings.chargeCurrent
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.chargeCurrent">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.lowTemperature') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.lowTemperature,
|
||||
'text-bg-success': !batteryData.alarms.lowTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.lowTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.lowTemperature,
|
||||
'text-bg-success': !batteryData.warnings.lowTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.lowTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.highTemperature') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.highTemperature,
|
||||
'text-bg-success': !batteryData.alarms.highTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.highTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.highTemperature,
|
||||
'text-bg-success': !batteryData.warnings.highTemperature
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.highTemperature">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.lowVoltage') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.lowVoltage,
|
||||
'text-bg-success': !batteryData.alarms.lowVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.lowVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.lowVoltage,
|
||||
'text-bg-success': !batteryData.warnings.lowVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.lowVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.highVoltage') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.highVoltage,
|
||||
'text-bg-success': !batteryData.alarms.highVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.highVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.highVoltage,
|
||||
'text-bg-success': !batteryData.warnings.highVoltage
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.highVoltage">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('battery.bmsInternal') }}</th>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-danger': batteryData.alarms.bmsInternal,
|
||||
'text-bg-success': !batteryData.alarms.bmsInternal
|
||||
}">
|
||||
<template v-if="!batteryData.alarms.bmsInternal">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.alarm') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="{
|
||||
'text-bg-warning text-dark': batteryData.warnings.bmsInternal,
|
||||
'text-bg-success': !batteryData.warnings.bmsInternal
|
||||
}">
|
||||
<template v-if="!batteryData.warnings.bmsInternal">{{ $t('battery.ok') }}</template>
|
||||
<template v-else>{{ $t('battery.warning') }}</template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Battery } from '@/types/BatteryDataStatus';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
batteryData: {} as Battery,
|
||||
isFirstFetchAfterConnect: true,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Battery");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/battery/livedata", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.batteryData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Battery WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/batterylivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.batteryData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Battery websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.batteryData) {
|
||||
this.batteryData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -17,9 +17,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ $t('firmwareinfo.FirmwareVersion') }}</th>
|
||||
<td><a :href="'https://github.com/tbnobody/OpenDTU/commits/' + systemStatus.git_hash?.substring(1)"
|
||||
<td><a :href="'https://github.com/tbnobody/OpenDTU/commits/' + systemStatus.git_hash"
|
||||
target="_blank" v-tooltip :title="$t('firmwareinfo.FirmwareVersionHint')">
|
||||
{{ systemStatus.git_hash?.substring(1) }}
|
||||
{{ systemStatus.git_hash }}
|
||||
</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
358
webapp/src/components/HuaweiView.vue
Normal file
358
webapp/src/components/HuaweiView.vue
Normal file
@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="text-center" v-if="dataLoading">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="row gy-3">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" :class="{
|
||||
'text-bg-danger': huaweiData.data_age > 20,
|
||||
'text-bg-primary': huaweiData.data_age < 19,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
Huawei R4850G2
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('huawei.DataAge') }} {{ $t('huawei.Seconds', { 'val': huaweiData.data_age }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-toolbar p-2" role="toolbar">
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button :disabled="false" type="button" class="btn btn-sm btn-danger" @click="onShowLimitSettings()"
|
||||
v-tooltip :title="$t('huawei.ShowSetLimit')">
|
||||
<BIconSpeedometer style="font-size:24px;" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Input') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_voltage') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_voltage.v) }}</td>
|
||||
<td>{{ huaweiData.input_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_current') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_current.v) }}</td>
|
||||
<td>{{ huaweiData.input_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_power') }}</th>
|
||||
<td style="text-align: right">{{ formatNumber(huaweiData.input_power.v) }}</td>
|
||||
<td>{{ huaweiData.input_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.input_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.input_temp.v) }}</td>
|
||||
<td>{{ huaweiData.input_temp.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.efficiency') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.efficiency.v.toFixed(3) }}</td>
|
||||
<td>{{ huaweiData.efficiency.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header bg-info">{{ $t('huawei.Output') }}</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('huawei.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('huawei.Value') }}</th>
|
||||
<th scope="col">{{ $t('huawei.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_voltage') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_voltage.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_voltage.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_current.v.toFixed(2) }}</td>
|
||||
<td>{{ huaweiData.output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.max_output_current') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.max_output_current.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.max_output_current.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_power') }}</th>
|
||||
<td style="text-align: right">{{ huaweiData.output_power.v.toFixed(1) }}</td>
|
||||
<td>{{ huaweiData.output_power.u }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('huawei.output_temp') }}</th>
|
||||
<td style="text-align: right">{{ Math.round(huaweiData.output_temp.v) }}</td>
|
||||
<td>{{ huaweiData.output_temp.u }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="huaweiLimitSettingView" ref="huaweiLimitSettingView" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form @submit="onSubmitLimit">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ $t('huawei.LimitSettings') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputCurrentLimit" class="col-sm-3 col-form-label">{{ $t('huawei.CurrentLimit') }} </label>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputVoltageTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetVoltageLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchVoltage"
|
||||
v-model="targetLimitList.voltage_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" name="inputVoltageTargetLimit" class="form-control" id="inputVoltageTargetLimit"
|
||||
:min="targetVoltageLimitMin" :max="targetVoltageLimitMax" v-model="targetLimitList.voltage"
|
||||
:disabled=!targetLimitList.voltage_valid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9">
|
||||
<div v-if="targetLimitList.voltage < targetVoltageLimitMinOffline" class="alert alert-secondary mt-3"
|
||||
role="alert" v-html="$t('huawei.LimitHint')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-center">
|
||||
<label for="inputCurrentTargetLimit" class="col-sm-3 col-form-label">{{ $t('huawei.SetCurrentLimit')
|
||||
}}</label>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<div class="form-switch form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="flexSwitchCurrentt"
|
||||
v-model="targetLimitList.current_valid">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input type="number" name="inputCurrentTargetLimit" class="form-control" id="inputCurrentTargetLimit"
|
||||
:min="targetCurrentLimitMin" :max="targetCurrentLimitMax" v-model="targetLimitList.current"
|
||||
:disabled=!targetLimitList.current_valid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(true)">{{ $t('huawei.SetOnline')
|
||||
}}</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger" @click="onSetLimitSettings(false)">{{
|
||||
$t('huawei.SetOffline')
|
||||
}}</button>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('huawei.Close') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { Huawei } from '@/types/HuaweiDataStatus';
|
||||
import type { HuaweiLimitConfig } from '@/types/HuaweiLimitConfig';
|
||||
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
|
||||
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import {
|
||||
BIconSpeedometer,
|
||||
} from 'bootstrap-icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BIconSpeedometer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataAgeInterval: 0,
|
||||
dataLoading: true,
|
||||
huaweiData: {} as Huawei,
|
||||
isFirstFetchAfterConnect: true,
|
||||
targetVoltageLimitMin: 42,
|
||||
targetVoltageLimitMinOffline: 48,
|
||||
targetVoltageLimitMax: 58,
|
||||
targetCurrentLimitMin: 0,
|
||||
targetCurrentLimitMax: 60,
|
||||
targetLimitList: {} as HuaweiLimitConfig,
|
||||
targetLimitPersistent: false,
|
||||
huaweiLimitSettingView: {} as bootstrap.Modal,
|
||||
|
||||
alertMessageLimit: "",
|
||||
alertTypeLimit: "info",
|
||||
showAlertLimit: false,
|
||||
checked: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getInitialData();
|
||||
this.initSocket();
|
||||
this.initDataAgeing();
|
||||
},
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
methods: {
|
||||
getInitialData() {
|
||||
console.log("Get initalData for Huawei");
|
||||
this.dataLoading = true;
|
||||
|
||||
fetch("/api/huaweilivedata/status", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.huaweiData = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
initSocket() {
|
||||
console.log("Starting connection to Huawei WebSocket Server");
|
||||
|
||||
const { protocol, host } = location;
|
||||
const authString = authUrl();
|
||||
const webSocketUrl = `${protocol === "https:" ? "wss" : "ws"
|
||||
}://${authString}${host}/huaweilivedata`;
|
||||
|
||||
this.socket = new WebSocket(webSocketUrl);
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
this.huaweiData = JSON.parse(event.data);
|
||||
this.dataLoading = false;
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfully connected to the Huawei websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
window.onbeforeunload = () => {
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
initDataAgeing() {
|
||||
this.dataAgeInterval = setInterval(() => {
|
||||
if (this.huaweiData) {
|
||||
this.huaweiData.data_age++;
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
// Send heartbeat packets regularly * 59s Send a heartbeat
|
||||
heartCheck() {
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.heartInterval = setInterval(() => {
|
||||
if (this.socket.readyState === 1) {
|
||||
// Connection status
|
||||
this.socket.send("ping");
|
||||
} else {
|
||||
this.initSocket(); // Breakpoint reconnection 5 Time
|
||||
}
|
||||
}, 59 * 1000);
|
||||
},
|
||||
/** To break off websocket Connect */
|
||||
closeSocket() {
|
||||
this.socket.close();
|
||||
this.heartInterval && clearTimeout(this.heartInterval);
|
||||
this.isFirstFetchAfterConnect = true;
|
||||
},
|
||||
formatNumber(num: number) {
|
||||
return new Intl.NumberFormat(
|
||||
undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||
).format(num);
|
||||
},
|
||||
onHideLimitSettings() {
|
||||
this.showAlertLimit = false;
|
||||
},
|
||||
onShowLimitSettings() {
|
||||
this.huaweiLimitSettingView = new bootstrap.Modal('#huaweiLimitSettingView');
|
||||
this.huaweiLimitSettingView.show();
|
||||
},
|
||||
onSetLimitSettings(online: boolean) {
|
||||
this.targetLimitList.online = online;
|
||||
},
|
||||
onSubmitLimit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.targetLimitList));
|
||||
|
||||
console.log(this.targetLimitList);
|
||||
|
||||
fetch("/api/huawei/limit/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
if (response.type == "success") {
|
||||
this.huaweiLimitSettingView.hide();
|
||||
} else {
|
||||
this.alertMessageLimit = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertTypeLimit = response.type;
|
||||
this.showAlertLimit = true;
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -1,57 +1,123 @@
|
||||
<template>
|
||||
<div v-show="totalVeData.enabled">
|
||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalYieldTotal') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.YieldTotal.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.YieldTotal.d,
|
||||
maximumFractionDigits: totalData.YieldTotal.d
|
||||
})}}
|
||||
<small class="text-muted">{{ totalData.YieldTotal.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.MpptTotalYieldTotal') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.YieldTotal.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldTotal.d,
|
||||
maximumFractionDigits: totalVeData.total.YieldTotal.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalVeData.total.YieldTotal.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalYieldDay') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.YieldDay.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.YieldDay.d,
|
||||
maximumFractionDigits: totalData.YieldDay.d
|
||||
})}}
|
||||
<small class="text-muted">{{ totalData.YieldDay.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.MpptTotalYieldDay') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.YieldDay.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.YieldDay.d,
|
||||
maximumFractionDigits: totalVeData.total.YieldDay.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalVeData.total.YieldDay.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.TotalPower') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.Power.d,
|
||||
maximumFractionDigits: totalData.Power.d
|
||||
})}}
|
||||
<small class="text-muted">{{ totalData.Power.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.MpptTotalPower') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalVeData.total.Power.v, 'decimal', {
|
||||
minimumFractionDigits: totalVeData.total.Power.d,
|
||||
maximumFractionDigits: totalVeData.total.Power.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalVeData.total.Power.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.InverterTotalYieldTotal') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.YieldTotal.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.YieldTotal.d,
|
||||
maximumFractionDigits: totalData.YieldTotal.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalData.YieldTotal.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.InverterTotalYieldDay') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.YieldDay.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.YieldDay.d,
|
||||
maximumFractionDigits: totalData.YieldDay.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalData.YieldDay.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.InverterTotalPower') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalData.Power.v, 'decimal', {
|
||||
minimumFractionDigits: totalData.Power.d,
|
||||
maximumFractionDigits: totalData.Power.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalData.Power.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="totalBattData.enabled">
|
||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-success">{{ $t('invertertotalinfo.BatterySoc') }}</div>
|
||||
<div class="card-body card-text text-center">
|
||||
<h2>
|
||||
{{ $n(totalBattData.soc.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.soc.d,
|
||||
maximumFractionDigits: totalBattData.soc.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalBattData.soc.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Total } from '@/types/LiveDataStatus';
|
||||
import type { Battery, Total, Vedirect } from '@/types/LiveDataStatus';
|
||||
import { defineComponent, type PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
totalData: { type: Object as PropType<Total>, required: true },
|
||||
},
|
||||
props: {
|
||||
totalData: { type: Object as PropType<Total>, required: true },
|
||||
totalVeData: { type: Object as PropType<Vedirect>, required: true },
|
||||
totalBattData: { type: Object as PropType<Battery>, required: true },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -60,6 +60,9 @@
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/battery">{{ $t('menu.BatterySettings') }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/chargerac">{{ $t('menu.AcChargerSettings') }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link @click="onClick" class="dropdown-item" to="/settings/device">{{ $t('menu.DeviceManager') }}</router-link>
|
||||
</li>
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
"DTUSettings": "DTU",
|
||||
"DeviceManager": "Hardware",
|
||||
"VedirectSettings": "Ve.direct",
|
||||
"BatterySettings": "Battery",
|
||||
"PowerMeterSettings": "Power Meter",
|
||||
"BatterySettings": "Batterie",
|
||||
"AcChargerSettings": "AC Ladegerät",
|
||||
"ConfigManagement": "Konfigurationsverwaltung",
|
||||
"FirmwareUpgrade": "Firmware-Aktualisierung",
|
||||
"DeviceReboot": "Neustart",
|
||||
@ -26,7 +28,8 @@
|
||||
"Login": "Anmelden"
|
||||
},
|
||||
"base": {
|
||||
"Loading": "Lade..."
|
||||
"Loading": "Lade...",
|
||||
"Reload": "Aktualisieren"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Dunkel",
|
||||
@ -323,9 +326,13 @@
|
||||
"Unit": "Einheit"
|
||||
},
|
||||
"invertertotalinfo": {
|
||||
"TotalYieldTotal": "Gesamtertrag Insgesamt",
|
||||
"TotalYieldDay": "Gesamtertrag Heute",
|
||||
"TotalPower": "Gesamtleistung"
|
||||
"InverterTotalYieldTotal": "Inverter Gesamtertrag Insgesamt",
|
||||
"InverterTotalYieldDay": "Inverter Gesamtertrag Heute",
|
||||
"InverterTotalPower": "Inverter Gesamtleistung",
|
||||
"MpptTotalYieldTotal": "MPPT Gesamtertrag Insgesamt",
|
||||
"MpptTotalYieldDay": "MPPT Gesamtertrag Heute",
|
||||
"MpptTotalPower": "MPPT Gesamtleistung",
|
||||
"BatterySoc": "Ladezustand"
|
||||
},
|
||||
"inverterchannelproperty": {
|
||||
"Power": "Leistung",
|
||||
@ -462,13 +469,27 @@
|
||||
"typeMQTT": "MQTT",
|
||||
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
|
||||
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
||||
"typeHTTP": "HTTP(S) + JSON",
|
||||
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1",
|
||||
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)",
|
||||
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)",
|
||||
"SDM": "SDM-Stromzähler Konfiguration",
|
||||
"sdmbaudrate": "Baudrate",
|
||||
"sdmaddress": "Modbus Adresse",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
"Save": "@:dtuadmin.Save",
|
||||
"HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration",
|
||||
"httpIndividualRequests": "Individuelle HTTP requests pro Phase",
|
||||
"httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)! Beispiele gibt es unten.",
|
||||
"httpPhase": "HTTP(S) + JSON Konfiguration - Phase {phaseNumber}",
|
||||
"httpEnabled": "Phase aktiviert",
|
||||
"httpUrl": "URL",
|
||||
"httpHeaderKey": "Optional: HTTP request header - Key",
|
||||
"httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.",
|
||||
"httpHeaderValue": "Optional: HTTP request header - Wert",
|
||||
"httpJsonPath": "JSON Pfad",
|
||||
"httpJsonPathDescription": "JSON Pfad um den Leistungswert zu finden. Es verwendet die Selektions-Syntax von mobizt/FirebaseJson. Beispiele gibt es unten.",
|
||||
"httpTimeout": "Timeout",
|
||||
"testHttpRequest": "Testen"
|
||||
},
|
||||
"powerlimiteradmin": {
|
||||
"PowerLimiterSettings": "Power Limiter Einstellungen",
|
||||
@ -630,5 +651,68 @@
|
||||
"Name": "Name",
|
||||
"ValueSelected": "Ausgewählt",
|
||||
"ValueActive": "Aktiv"
|
||||
}
|
||||
},
|
||||
"huawei": {
|
||||
"DataAge": "letzte Aktualisierung: ",
|
||||
"Seconds": "vor {val} Sekunden",
|
||||
"Input": "Eingang",
|
||||
"Output": "Ausgang",
|
||||
"Property": "Eigenschaft",
|
||||
"Value": "Wert",
|
||||
"Unit": "Einheit",
|
||||
"input_voltage": "Eingangsspannung",
|
||||
"input_current": "Eingangsstrom",
|
||||
"input_power": "Eingangsleistung",
|
||||
"input_temp": "Eingangstemperatur",
|
||||
"efficiency": "Wirkungsgrad",
|
||||
"output_voltage": "Ausgangsspannung",
|
||||
"output_current": "Ausgangsstrom",
|
||||
"max_output_current": "Maximaler Ausgangsstrom",
|
||||
"output_power": "Ausgangsleistung",
|
||||
"output_temp": "Ausgangstemperatur",
|
||||
"ShowSetLimit": "Zeige / Setze Limit",
|
||||
"LimitSettings": "Limit-Einstellungen",
|
||||
"SetOffline": "Limit setzen, CAN Bus nicht verbunden",
|
||||
"SetOnline": "Limit setzen, CAN Bus verbunden",
|
||||
"LimitHint": "<b>Hinweis:</b> Spannungsbereich wenn CAN Bus nicht verbunden ist 48V-58.5V ",
|
||||
"Close": "Schließen",
|
||||
"SetVoltageLimit": "Spannungslimit:",
|
||||
"SetCurrentLimit": "Stromlimit:",
|
||||
"CurrentLimit": "Aktuelles Limit: "
|
||||
},
|
||||
"acchargeradmin": {
|
||||
"ChargerSettings": "AC Ladegerät Einstellungen",
|
||||
"Configuration": "AC Ladegerät Konfiguration",
|
||||
"EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv",
|
||||
"Seconds": "@:dtuadmin.Seconds",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
"battery": {
|
||||
"battery": "Batterie",
|
||||
"DataAge": "letzte Aktualisierung: ",
|
||||
"Seconds": "vor {val} Sekunden",
|
||||
"Status": "Status",
|
||||
"Property": "Eigenschaft",
|
||||
"Value": "Wert",
|
||||
"Unit": "Einheit",
|
||||
"stateOfCharge": "Ladezustand (SOC)",
|
||||
"stateOfHealth": "Batteriezustand (SOH)",
|
||||
"voltage": "Spannung",
|
||||
"current": "Strom",
|
||||
"temperature": "Temperatur",
|
||||
"chargeVoltage": "Gewünschte Ladespannung (BMS)",
|
||||
"chargeCurrentLimitation": "Ladestromlimit",
|
||||
"dischargeCurrentLimitation": "Entladestromlimit",
|
||||
"warn_alarm": "Warnungen und Alarme",
|
||||
"ok": "OK",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warnung",
|
||||
"dischargeCurrent": "Entladestrom",
|
||||
"chargeCurrent": "Ladestrom",
|
||||
"lowTemperature": "Temperatur niedrig",
|
||||
"highTemperature": "Temperatur hoch",
|
||||
"lowVoltage": "Spannung niedrig",
|
||||
"highVoltage": "Spannung hoch",
|
||||
"bmsInternal": "BMS intern"
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"VedirectSettings": "Ve.direct Settings",
|
||||
"PowerMeterSettings": "Power Meter Settings",
|
||||
"BatterySettings": "@:batteryadmin.BatterySettings",
|
||||
"AcChargerSettings": "AC Charger",
|
||||
"ConfigManagement": "Config Management",
|
||||
"FirmwareUpgrade": "Firmware Upgrade",
|
||||
"DeviceReboot": "Device Reboot",
|
||||
@ -27,7 +28,8 @@
|
||||
"Login": "Login"
|
||||
},
|
||||
"base": {
|
||||
"Loading": "Loading..."
|
||||
"Loading": "Loading...",
|
||||
"Reload": "Reload"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Dark",
|
||||
@ -324,9 +326,13 @@
|
||||
"Unit": "Unit"
|
||||
},
|
||||
"invertertotalinfo": {
|
||||
"TotalYieldTotal": "Total Yield Total",
|
||||
"TotalYieldDay": "Total Yield Day",
|
||||
"TotalPower": "Total Power"
|
||||
"InverterTotalYieldTotal": "Inverter Total Yield Total",
|
||||
"InverterTotalYieldDay": "Inverter Total Yield Day",
|
||||
"InverterTotalPower": "Inverter Total Power",
|
||||
"MpptTotalYieldTotal": "MPPT Total Yield Total",
|
||||
"MpptTotalYieldDay": "MPPT Total Yield Day",
|
||||
"MpptTotalPower": "MPPT Total Power",
|
||||
"BatterySoc": "State of charge"
|
||||
},
|
||||
"inverterchannelproperty": {
|
||||
"Power": "Power",
|
||||
@ -463,13 +469,28 @@
|
||||
"typeMQTT": "MQTT",
|
||||
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
|
||||
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
|
||||
"typeHTTP": "HTTP(s) + JSON",
|
||||
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
|
||||
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
|
||||
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
|
||||
"SDM": "SDM-Power Meter Parameter",
|
||||
"sdmbaudrate": "Baudrate",
|
||||
"sdmaddress": "Modbus Address",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
"Save": "@:dtuadmin.Save",
|
||||
"HTTP": "HTTP(S) + Json - General configuration",
|
||||
"httpIndividualRequests": "Individual HTTP requests per phase",
|
||||
"httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}",
|
||||
"httpEnabled": "Phase enabled",
|
||||
"httpUrl": "URL",
|
||||
"httpUrlDescription": "URL must start with http:// or https://. Some characters like spaces and = have to be encoded with URL encoding (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)! See below for some examples.",
|
||||
"httpHeaderKey": "Optional: HTTP request header - Key",
|
||||
"httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.",
|
||||
"httpHeaderValue": "Optional: HTTP request header - Value",
|
||||
"httpJsonPath": "Json path",
|
||||
"httpJsonPathDescription": "JSON path to find the power value in the response. This uses the JSON path query syntax from mobizt/FirebaseJson. See below for some examples.",
|
||||
"httpTimeout": "Timeout",
|
||||
"testHttpRequest": "Run test",
|
||||
"milliSeconds": "ms"
|
||||
},
|
||||
"powerlimiteradmin": {
|
||||
"PowerLimiterSettings": "Power Limiter Settings",
|
||||
@ -635,5 +656,68 @@
|
||||
"Number": "Number",
|
||||
"ValueSelected": "Selected",
|
||||
"ValueActive": "Active"
|
||||
}
|
||||
},
|
||||
"huawei": {
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
"Input": "Input",
|
||||
"Output": "Output",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"input_voltage": "Input voltage",
|
||||
"input_current": "Input current",
|
||||
"input_power": "Input power",
|
||||
"input_temp": "Input temperature",
|
||||
"efficiency": "Efficiency",
|
||||
"output_voltage": "Output voltage",
|
||||
"output_current": "Output current",
|
||||
"max_output_current": "Maximum output current",
|
||||
"output_power": "Output power",
|
||||
"output_temp": "Output temperature",
|
||||
"ShowSetLimit": "Show / Set Huawei Limit",
|
||||
"LimitSettings": "Limit Settings",
|
||||
"SetOffline": "Set limit, CAN bus not connected",
|
||||
"SetOnline": "Set limit, CAN bus connected",
|
||||
"LimitHint": "<b>Hint:</b> CAN bus not connected voltage limit is 48V-58.5V.",
|
||||
"Close": "close",
|
||||
"SetVoltageLimit": "Voltage limit:",
|
||||
"SetCurrentLimit": "Current limit:",
|
||||
"CurrentLimit": "Current limit:"
|
||||
},
|
||||
"acchargeradmin": {
|
||||
"ChargerSettings": "AC Charger Settings",
|
||||
"Configuration": "AC Charger Configuration",
|
||||
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
||||
"Seconds": "@:dtuadmin.Seconds",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
"battery": {
|
||||
"battery": "battery",
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
"Status": "Status",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"stateOfCharge": "State of charge",
|
||||
"stateOfHealth": "State of health",
|
||||
"voltage": "Voltage",
|
||||
"current": "Current",
|
||||
"temperature": "Temperature",
|
||||
"chargeVoltage": "Requested charge voltage",
|
||||
"chargeCurrentLimitation": "Charge current limit",
|
||||
"dischargeCurrentLimitation": "Discharge current limit",
|
||||
"warn_alarm": "Alarms and warnings",
|
||||
"ok": "OK",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warning",
|
||||
"dischargeCurrent": "Discharge current",
|
||||
"chargeCurrent": "Charge current",
|
||||
"lowTemperature": "Low temperature",
|
||||
"highTemperature": "High temperature",
|
||||
"lowVoltage": "Low voltage",
|
||||
"highVoltage": "High voltage",
|
||||
"bmsInternal": "BMS internal"
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,9 @@
|
||||
"DTUSettings": "DTU",
|
||||
"DeviceManager": "Périphériques",
|
||||
"VedirectSettings": "Ve.direct",
|
||||
"PowerMeterSettings": "Power Meter",
|
||||
"BatterySettings": "Battery",
|
||||
"AcChargerSettings": "AC Charger",
|
||||
"ConfigManagement": "Gestion de la configuration",
|
||||
"FirmwareUpgrade": "Mise à jour du firmware",
|
||||
"DeviceReboot": "Redémarrage de l'appareil",
|
||||
@ -25,7 +28,8 @@
|
||||
"Login": "Connexion"
|
||||
},
|
||||
"base": {
|
||||
"Loading": "Chargement..."
|
||||
"Loading": "Chargement...",
|
||||
"Reload": "Reload"
|
||||
},
|
||||
"localeswitcher": {
|
||||
"Dark": "Dark",
|
||||
@ -321,9 +325,13 @@
|
||||
"Unit": "Unité"
|
||||
},
|
||||
"invertertotalinfo": {
|
||||
"TotalYieldTotal": "Rendement total",
|
||||
"TotalYieldDay": "Rendement du jour",
|
||||
"TotalPower": "Puissance de l'installation"
|
||||
"InverterTotalYieldTotal": "Onduleurs rendement total",
|
||||
"InverterTotalYieldDay": "Onduleurs rendement du jour",
|
||||
"InverterTotalPower": "Onduleurs puissance de l'installation",
|
||||
"MpptTotalYieldTotal": "MPPT rendement total",
|
||||
"MpptTotalYieldDay": "MPPT rendement du jour",
|
||||
"MpptTotalPower": "MPPT puissance de l'installation",
|
||||
"BatterySoc": "State of charge"
|
||||
},
|
||||
"inverterchannelproperty": {
|
||||
"Power": "Puissance",
|
||||
@ -571,5 +579,68 @@
|
||||
"Name": "Nom",
|
||||
"ValueSelected": "Sélectionné",
|
||||
"ValueActive": "Activé"
|
||||
}
|
||||
},
|
||||
"huawei": {
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
"Input": "Input",
|
||||
"Output": "Output",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"input_voltage": "Input voltage",
|
||||
"input_current": "Input current",
|
||||
"input_power": "Input power",
|
||||
"input_temp": "Input temperature",
|
||||
"efficiency": "Efficiency",
|
||||
"output_voltage": "Output voltage",
|
||||
"output_current": "Output current",
|
||||
"max_output_current": "Maximum output current",
|
||||
"output_power": "Output power",
|
||||
"output_temp": "Output temperature",
|
||||
"ShowSetLimit": "Show / Set Huawei Limit",
|
||||
"LimitSettings": "Limit Settings",
|
||||
"SetOffline": "Set limit, CAN bus not connected",
|
||||
"SetOnline": "Set limit, CAN bus connected",
|
||||
"LimitHint": "<b>Hint:</b> CAN bus not connected voltage limit is 48V-58.5V.",
|
||||
"Close": "close",
|
||||
"SetVoltageLimit": "Voltage limit:",
|
||||
"SetCurrentLimit": "Current limit:",
|
||||
"CurrentLimit": "Current limit:"
|
||||
},
|
||||
"acchargeradmin": {
|
||||
"ChargerSettings": "AC Charger Settings",
|
||||
"Configuration": "AC Charger Configuration",
|
||||
"EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface",
|
||||
"Seconds": "@:dtuadmin.Seconds",
|
||||
"Save": "@:dtuadmin.Save"
|
||||
},
|
||||
"battery": {
|
||||
"battery": "battery",
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": " {val} seconds",
|
||||
"Status": "Status",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"stateOfCharge": "State of charge",
|
||||
"stateOfHealth": "State of health",
|
||||
"voltage": "Voltage",
|
||||
"current": "Current",
|
||||
"temperature": "Temperature",
|
||||
"chargeVoltage": "Requested charge voltage",
|
||||
"chargeCurrentLimitation": "Charge current limit",
|
||||
"dischargeCurrentLimitation": "Discharge current limit",
|
||||
"warn_alarm": "Alarms and warnings",
|
||||
"ok": "OK",
|
||||
"alarm": "Alarm",
|
||||
"warning": "Warning",
|
||||
"dischargeCurrent": "Discharge current",
|
||||
"chargeCurrent": "Charge current",
|
||||
"lowTemperature": "Low temperature",
|
||||
"highTemperature": "High temperature",
|
||||
"lowVoltage": "Low voltage",
|
||||
"highVoltage": "High voltage",
|
||||
"bmsInternal": "BMS internal"
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import AboutView from '@/views/AboutView.vue';
|
||||
import BatteryAdminView from '@/views/BatteryAdminView.vue';
|
||||
import AcChargerAdminView from '@/views/AcChargerAdminView.vue';
|
||||
import ConfigAdminView from '@/views/ConfigAdminView.vue';
|
||||
import ConsoleInfoView from '@/views/ConsoleInfoView.vue';
|
||||
import DeviceAdminView from '@/views/DeviceAdminView.vue'
|
||||
@ -102,6 +103,11 @@ const router = createRouter({
|
||||
name: 'Battery Settings',
|
||||
component: BatteryAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/chargerac',
|
||||
name: 'Charger Settings',
|
||||
component: AcChargerAdminView
|
||||
},
|
||||
{
|
||||
path: '/settings/mqtt',
|
||||
name: 'MqTT Settings',
|
||||
|
||||
3
webapp/src/types/AcChargerConfig.ts
Normal file
3
webapp/src/types/AcChargerConfig.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface AcChargerConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
32
webapp/src/types/BatteryDataStatus.ts
Normal file
32
webapp/src/types/BatteryDataStatus.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
interface BatteryFlags {
|
||||
dischargeCurrent: boolean;
|
||||
chargeCurrent: boolean;
|
||||
lowTemperature: boolean;
|
||||
highTemperature: boolean;
|
||||
lowVoltage: boolean;
|
||||
highVoltage: boolean;
|
||||
bmsInternal: boolean;
|
||||
}
|
||||
|
||||
|
||||
// Battery
|
||||
export interface Battery {
|
||||
data_age: 0;
|
||||
chargeVoltage: ValueObject;
|
||||
chargeCurrentLimitation: ValueObject;
|
||||
dischargeCurrentLimitation: ValueObject;
|
||||
stateOfCharge: ValueObject;
|
||||
stateOfChargeLastUpdate: ValueObject;
|
||||
stateOfHealth: ValueObject;
|
||||
voltage: ValueObject;
|
||||
current: ValueObject;
|
||||
temperature: ValueObject;
|
||||
warnings: BatteryFlags;
|
||||
alarms: BatteryFlags;
|
||||
manufacturer: string;
|
||||
chargeEnabled: boolean;
|
||||
dischargeEnabled: boolean;
|
||||
chargeImmediately: boolean;
|
||||
}
|
||||
18
webapp/src/types/HuaweiDataStatus.ts
Normal file
18
webapp/src/types/HuaweiDataStatus.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { ValueObject } from '@/types/LiveDataStatus';
|
||||
|
||||
// Huawei
|
||||
export interface Huawei {
|
||||
data_age: 0;
|
||||
input_voltage: ValueObject;
|
||||
input_frequency: ValueObject;
|
||||
input_current: ValueObject;
|
||||
input_power: ValueObject;
|
||||
input_temp: ValueObject;
|
||||
efficiency: ValueObject;
|
||||
output_voltage: ValueObject;
|
||||
output_current: ValueObject;
|
||||
max_output_current: ValueObject;
|
||||
output_power: ValueObject;
|
||||
output_temp: ValueObject;
|
||||
amp_hour: ValueObject;
|
||||
}
|
||||
7
webapp/src/types/HuaweiLimitConfig.ts
Normal file
7
webapp/src/types/HuaweiLimitConfig.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface HuaweiLimitConfig {
|
||||
voltage: number;
|
||||
voltage_valid: boolean;
|
||||
current: number;
|
||||
current_valid: boolean;
|
||||
online: boolean;
|
||||
}
|
||||
@ -24,6 +24,7 @@ export interface Inverter {
|
||||
serial: number;
|
||||
name: string;
|
||||
data_age: number;
|
||||
poll_enabled: boolean;
|
||||
reachable: boolean;
|
||||
producing: boolean;
|
||||
limit_relative: number;
|
||||
@ -48,6 +49,16 @@ export interface Hints {
|
||||
|
||||
export interface Vedirect {
|
||||
enabled: boolean;
|
||||
total: Total;
|
||||
}
|
||||
|
||||
export interface Huawei {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface Battery {
|
||||
enabled: boolean;
|
||||
soc: ValueObject;
|
||||
}
|
||||
|
||||
export interface LiveData {
|
||||
@ -55,4 +66,6 @@ export interface LiveData {
|
||||
total: Total;
|
||||
hints: Hints;
|
||||
vedirect: Vedirect;
|
||||
huawei: Huawei;
|
||||
battery: Battery;
|
||||
}
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
export interface PowerMeterHttpPhaseConfig {
|
||||
index: number;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
header_key: string;
|
||||
header_value: string;
|
||||
json_path: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
export interface PowerMeterConfig {
|
||||
enabled: boolean;
|
||||
source: number;
|
||||
@ -7,4 +17,6 @@ export interface PowerMeterConfig {
|
||||
mqtt_topic_powermeter_3: string;
|
||||
sdmbaudrate: number;
|
||||
sdmaddress: number;
|
||||
http_individual_requests: boolean;
|
||||
http_phases: Array<PowerMeterHttpPhaseConfig>;
|
||||
}
|
||||
|
||||
79
webapp/src/views/AcChargerAdminView.vue
Normal file
79
webapp/src/views/AcChargerAdminView.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<BasePage :title="$t('acchargeradmin.ChargerSettings')" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="saveChargerConfig">
|
||||
<CardElement :text="$t('acchargeradmin.Configuration')" textVariant="text-bg-primary">
|
||||
<InputElement :label="$t('acchargeradmin.EnableHuawei')"
|
||||
v-model="acChargerConfigList.enabled"
|
||||
type="checkbox" wide/>
|
||||
</CardElement>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-3">{{ $t('acchargeradmin.Save') }}</button>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import type { AcChargerConfig } from "@/types/AcChargerConfig";
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
CardElement,
|
||||
InputElement,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dataLoading: true,
|
||||
acChargerConfigList: {} as AcChargerConfig,
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getChargerConfig();
|
||||
},
|
||||
methods: {
|
||||
getChargerConfig() {
|
||||
this.dataLoading = true;
|
||||
fetch("/api/huawei/config", { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.acChargerConfigList = data;
|
||||
this.dataLoading = false;
|
||||
});
|
||||
},
|
||||
saveChargerConfig(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(this.acChargerConfigList));
|
||||
|
||||
fetch("/api/huawei/config", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@ -87,7 +87,7 @@ export default defineComponent({
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfuly connected to the echo websocket server...");
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BasePage :title="$t('home.LiveData')" :isLoading="dataLoading" :isWideScreen="true">
|
||||
<HintView :hints="liveData.hints" />
|
||||
<InverterTotalInfo :totalData="liveData.total" /><br />
|
||||
<InverterTotalInfo :totalData="liveData.total" :totalVeData="liveData.vedirect" :totalBattData="liveData.battery"/><br />
|
||||
<div class="row gy-3">
|
||||
<div class="col-sm-3 col-md-2" :style="[inverterData.length == 1 ? { 'display': 'none' } : {}]">
|
||||
<div class="nav nav-pills row-cols-sm-1" id="v-pills-tab" role="tablist" aria-orientation="vertical">
|
||||
@ -27,9 +27,10 @@
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center"
|
||||
:class="{
|
||||
'text-bg-danger': !inverter.reachable,
|
||||
'text-bg-warning': inverter.reachable && !inverter.producing,
|
||||
'text-bg-primary': inverter.reachable && inverter.producing,
|
||||
'text-bg-tertiary': !inverter.poll_enabled,
|
||||
'text-bg-danger': inverter.poll_enabled && !inverter.reachable,
|
||||
'text-bg-warning': inverter.poll_enabled && inverter.reachable && !inverter.producing,
|
||||
'text-bg-primary': inverter.poll_enabled && inverter.reachable && inverter.producing,
|
||||
}">
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
@ -114,6 +115,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<VedirectView v-show="liveData.vedirect.enabled" />
|
||||
<div v-show="liveData.battery.enabled" >
|
||||
<BatteryView/>
|
||||
</div>
|
||||
<div v-show="liveData.huawei.enabled" >
|
||||
<HuaweiView/>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<div class="modal" id="eventView" tabindex="-1">
|
||||
@ -325,6 +332,8 @@ import HintView from '@/components/HintView.vue';
|
||||
import InverterChannelInfo from "@/components/InverterChannelInfo.vue";
|
||||
import InverterTotalInfo from '@/components/InverterTotalInfo.vue';
|
||||
import VedirectView from '@/components/VedirectView.vue';
|
||||
import HuaweiView from '@/components/HuaweiView.vue'
|
||||
import BatteryView from '@/components/BatteryView.vue'
|
||||
import type { DevInfoStatus } from '@/types/DevInfoStatus';
|
||||
import type { EventlogItems } from '@/types/EventlogStatus';
|
||||
import type { LimitConfig } from '@/types/LimitConfig';
|
||||
@ -365,7 +374,9 @@ export default defineComponent({
|
||||
BIconToggleOff,
|
||||
BIconToggleOn,
|
||||
BIconXCircleFill,
|
||||
VedirectView
|
||||
VedirectView,
|
||||
HuaweiView,
|
||||
BatteryView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -492,7 +503,7 @@ export default defineComponent({
|
||||
|
||||
this.socket.onopen = function (event) {
|
||||
console.log(event);
|
||||
console.log("Successfuly connected to the echo websocket server...");
|
||||
console.log("Successfully connected to the echo websocket server...");
|
||||
};
|
||||
|
||||
// Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BasePage :title="$t('mqttinfo.MqttInformation')" :isLoading="dataLoading">
|
||||
<BasePage :title="$t('mqttinfo.MqttInformation')" :isLoading="dataLoading" :show-reload="true" @reload="getMqttInfo">
|
||||
<CardElement :text="$t('mqttinfo.ConfigurationSummary')" textVariant="text-bg-primary">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BasePage :title="$t('networkinfo.NetworkInformation')" :isLoading="dataLoading">
|
||||
<BasePage :title="$t('networkinfo.NetworkInformation')" :isLoading="dataLoading" :show-reload="true" @reload="getNetworkInfo">
|
||||
<WifiStationInfo :networkStatus="networkDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<WifiApInfo :networkStatus="networkDataList" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BasePage :title="$t('ntpinfo.NtpInformation')" :isLoading="dataLoading">
|
||||
<BasePage :title="$t('ntpinfo.NtpInformation')" :isLoading="dataLoading" :show-reload="true" @reload="getNtpInfo">
|
||||
<CardElement :text="$t('ntpinfo.ConfigurationSummary')" textVariant="text-bg-primary">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
|
||||
@ -1,24 +1,22 @@
|
||||
<template>
|
||||
<BasePage :title="$t('powermeteradmin.PowerMeterSettings')" :isLoading="dataLoading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType" ref="alert">
|
||||
{{ alertMessage }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<form @submit="savePowerMeterConfig">
|
||||
<div class="card">
|
||||
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.PowerMeterConfiguration') }}</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputPowerMeterEnable">{{ $t('powermeteradmin.PowerMeterEnable') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputPowerMeterEnable"
|
||||
v-model="powerMeterConfigList.enabled" />
|
||||
</div>
|
||||
<CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')"
|
||||
textVariant="text-bg-primary">
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-2 form-check-label" for="inputPowerMeterEnable">{{ $t('powermeteradmin.PowerMeterEnable') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="inputPowerMeterEnable"
|
||||
v-model="powerMeterConfigList.enabled" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3" v-show="powerMeterConfigList.enabled">
|
||||
</div>
|
||||
<div class="row mb-3" v-show="powerMeterConfigList.enabled">
|
||||
<label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.PowerMeterSource') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-select" v-model="powerMeterConfigList.source">
|
||||
@ -28,12 +26,13 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<div class="card" v-if="powerMeterConfigList.source === 0 && powerMeterConfigList.enabled" >
|
||||
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.MQTT') }}</div>
|
||||
<div class="card-body">
|
||||
<div v-if="powerMeterConfigList.enabled">
|
||||
<CardElement v-if="powerMeterConfigList.source === 0"
|
||||
:text="$t('powermeteradmin.MQTT')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
<div class="row mb-3">
|
||||
<label for="inputMqttTopicPowerMeter1" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter1') }}:</label>
|
||||
<div class="col-sm-10">
|
||||
@ -43,7 +42,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="inputMqttTopicPowerMeter2" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter2') }}:</label>
|
||||
<div class="col-sm-10">
|
||||
@ -63,13 +62,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
|
||||
<div class="card" v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2) && powerMeterConfigList.enabled" >
|
||||
<div class="card-header text-bg-primary">{{ $t('powermeteradmin.SDM') }}</div>
|
||||
<div class="card-body">
|
||||
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)"
|
||||
:text="$t('powermeteradmin.SDM')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
<div class="row mb-3">
|
||||
<label for="sdmbaudrate" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.sdmbaudrate') }}:</label>
|
||||
<div class="col-sm-10">
|
||||
@ -89,9 +87,96 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<div v-if="powerMeterConfigList.source === 3">
|
||||
<CardElement :text="$t('powermeteradmin.HTTP')"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
<InputElement :label="$t('powermeteradmin.httpIndividualRequests')"
|
||||
v-model="powerMeterConfigList.http_individual_requests"
|
||||
type="checkbox"
|
||||
wide />
|
||||
</CardElement>
|
||||
|
||||
<CardElement
|
||||
v-for="(http_phase, index) in powerMeterConfigList.http_phases"
|
||||
:text="$t('powermeteradmin.httpPhase', { phaseNumber: http_phase.index })"
|
||||
textVariant="text-bg-primary"
|
||||
add-space>
|
||||
<InputElement
|
||||
v-if="index > 0"
|
||||
:label="$t('powermeteradmin.httpEnabled')"
|
||||
v-model="http_phase.enabled"
|
||||
type="checkbox" wide />
|
||||
|
||||
<div v-if="http_phase.enabled">
|
||||
<div v-if="index == 0 || powerMeterConfigList.http_individual_requests">
|
||||
<InputElement :label="$t('powermeteradmin.httpUrl')"
|
||||
v-model="http_phase.url"
|
||||
type="text"
|
||||
maxlength="1024"
|
||||
placeholder="http://admin:supersecret@mypowermeter.home/status"
|
||||
prefix="GET "
|
||||
:tooltip="$t('powermeteradmin.httpUrlDescription')" />
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.httpHeaderKey')"
|
||||
v-model="http_phase.header_key"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:tooltip="$t('powermeteradmin.httpHeaderKeyDescription')" />
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.httpHeaderValue')"
|
||||
v-model="http_phase.header_value"
|
||||
type="text"
|
||||
maxlength="256" />
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.httpTimeout')"
|
||||
v-model="http_phase.timeout"
|
||||
type="number"
|
||||
:postfix="$t('powermeteradmin.milliSeconds')" />
|
||||
</div>
|
||||
|
||||
<InputElement :label="$t('powermeteradmin.httpJsonPath')"
|
||||
v-model="http_phase.json_path"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
placeholder="total_power"
|
||||
:tooltip="$t('powermeteradmin.httpJsonPathDescription')" />
|
||||
|
||||
<div class="text-center mb-3">
|
||||
<button type="button" class="btn btn-danger" @click="testHttpRequest(index)">
|
||||
{{ $t('powermeteradmin.testHttpRequest') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<BootstrapAlert v-model="testHttpRequestAlert[index].show" dismissible :variant="testHttpRequestAlert[index].type">
|
||||
{{ testHttpRequestAlert[index].message }}
|
||||
</BootstrapAlert>
|
||||
</div>
|
||||
</CardElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-3">{{ $t('powermeteradmin.Save') }}</button>
|
||||
|
||||
<div v-if="powerMeterConfigList.source === 3" class="alert alert-secondary" role="alert">
|
||||
<h2>URL examples:</h2>
|
||||
<ul>
|
||||
<li>http://admin:secret@shelly3em.home/status</li>
|
||||
<li>https://admin:secret@shelly3em.home/status</li>
|
||||
<li>http://tasmota-123.home/cm?cmnd=status%208</li>
|
||||
<li>http://12.34.56.78/emeter/0</li>
|
||||
</ul>
|
||||
|
||||
<h2>JSON path examples:</h2>
|
||||
<ul>
|
||||
<li>total_power - { "othervalue": "blah", "total_power": 123.4 }</li>
|
||||
<li>testarray/[2]/myvalue - { "testarray": [ {}, { "power": 123.4 } ] }</li>
|
||||
</ul>
|
||||
|
||||
More info: <a href="https://github.com/mobizt/FirebaseJson">https://github.com/mobizt/FirebaseJson</a>
|
||||
</div>
|
||||
</form>
|
||||
</BasePage>
|
||||
</template>
|
||||
@ -100,19 +185,37 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from "@/components/BootstrapAlert.vue";
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import InputElement from '@/components/InputElement.vue';
|
||||
import { handleResponse, authHeader } from '@/utils/authentication';
|
||||
import type { PowerMeterConfig } from "@/types/PowerMeterConfig";
|
||||
import { BIconInfoCircle } from 'bootstrap-icons-vue';
|
||||
import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
BootstrapAlert,
|
||||
CardElement,
|
||||
InputElement,
|
||||
BIconInfoCircle,
|
||||
},
|
||||
data() {
|
||||
const people: { name: string; age: number; }[] = [
|
||||
{
|
||||
age: 27,
|
||||
name: 'Tim'
|
||||
},
|
||||
{
|
||||
age: 28,
|
||||
name: 'Bob'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
dataLoading: true,
|
||||
powerMeterConfigList: {} as PowerMeterConfig,
|
||||
powerMeterSourceList: [
|
||||
{ key: 3, value: this.$t('powermeteradmin.typeHTTP') },
|
||||
{ key: 0, value: this.$t('powermeteradmin.typeMQTT') },
|
||||
{ key: 1, value: this.$t('powermeteradmin.typeSDM1ph') },
|
||||
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') },
|
||||
@ -120,6 +223,7 @@ export default defineComponent({
|
||||
alertMessage: "",
|
||||
alertType: "info",
|
||||
showAlert: false,
|
||||
testHttpRequestAlert: <{ message: string; type: string; show: boolean; }[]> [ ],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@ -133,6 +237,23 @@ export default defineComponent({
|
||||
.then((data) => {
|
||||
this.powerMeterConfigList = data;
|
||||
this.dataLoading = false;
|
||||
|
||||
type MyType = {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type MyGroupType = {
|
||||
[key:string]: MyType;
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.powerMeterConfigList.http_phases.length; i++) {
|
||||
this.testHttpRequestAlert.push({
|
||||
message: "",
|
||||
type: "",
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
savePowerMeterConfig(e: Event) {
|
||||
@ -152,9 +273,46 @@ export default defineComponent({
|
||||
this.alertMessage = response.message;
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
testHttpRequest(index: number) {
|
||||
var phaseConfig:PowerMeterHttpPhaseConfig;
|
||||
|
||||
if (this.powerMeterConfigList.http_individual_requests) {
|
||||
phaseConfig = this.powerMeterConfigList.http_phases[index];
|
||||
} else {
|
||||
phaseConfig = { ...this.powerMeterConfigList.http_phases[0] };
|
||||
phaseConfig.index = this.powerMeterConfigList.http_phases[index].index;
|
||||
phaseConfig.json_path = this.powerMeterConfigList.http_phases[index].json_path;
|
||||
}
|
||||
|
||||
this.testHttpRequestAlert[index] = {
|
||||
message: "Sending HTTP request...",
|
||||
type: "info",
|
||||
show: true,
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("data", JSON.stringify(phaseConfig));
|
||||
|
||||
fetch("/api/powermeter/testhttprequest", {
|
||||
method: "POST",
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then(
|
||||
(response) => {
|
||||
this.testHttpRequestAlert[index] = {
|
||||
message: response.message,
|
||||
type: response.type,
|
||||
show: true,
|
||||
};
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BasePage :title="$t('systeminfo.SystemInfo')" :isLoading="dataLoading">
|
||||
<BasePage :title="$t('systeminfo.SystemInfo')" :isLoading="dataLoading" :show-reload="true" @reload="getSystemInfo">
|
||||
<FirmwareInfo :systemStatus="systemDataList" />
|
||||
<div class="mt-5"></div>
|
||||
<HardwareInfo :systemStatus="systemDataList" />
|
||||
@ -50,8 +50,16 @@ export default defineComponent({
|
||||
})
|
||||
},
|
||||
getUpdateInfo() {
|
||||
// If the left char is a "g" the value is the git hash (remove the "g")
|
||||
this.systemDataList.git_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g' ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash;
|
||||
|
||||
// Handle format "v0.1-5-gabcdefh"
|
||||
if (this.systemDataList.git_hash.lastIndexOf("-") >= 0) {
|
||||
this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2)
|
||||
}
|
||||
|
||||
const fetchUrl = "https://api.github.com/repos/tbnobody/OpenDTU/compare/"
|
||||
+ this.systemDataList.git_hash?.substring(1) + "...HEAD";
|
||||
+ this.systemDataList.git_hash + "...HEAD";
|
||||
|
||||
fetch(fetchUrl)
|
||||
.then((response) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user