Merge branch 'pr/MalteSchm/144' into development

This commit is contained in:
helgeerbe 2023-03-27 22:16:53 +02:00
commit b3c17c8ee8
33 changed files with 1555 additions and 6 deletions

View File

@ -13,6 +13,7 @@ This project is still under development and adds following features:
* 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.
[![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml)
[![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml)
@ -166,6 +167,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

View File

@ -127,6 +127,7 @@ struct CONFIG_T {
float PowerLimiter_VoltageLoadCorrectionFactor;
bool Battery_Enabled;
bool Huawei_Enabled;
char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1];
bool Security_AllowReadonly;

95
include/Huawei_can.h Normal file
View 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();
unsigned long getLastUpdate();
private:
void sendRequest();
void onReceive(uint8_t* frame, uint8_t len);
unsigned long previousMillis;
unsigned long lastUpdate;
RectifierParameters_t _rp;
SPIClass *spi;
MCP_CAN *CAN;
uint8_t _huawei_irq;
uint8_t _huawei_power;
};
extern HuaweiCanClass HuaweiCan;

View 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;

View File

@ -33,6 +33,12 @@ 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;
};
class PinMappingClass {
@ -45,7 +51,8 @@ public:
bool isValidEthConfig();
bool isValidVictronConfig();
bool isValidBatteryConfig();
bool isValidHuaweiConfig();
private:
PinMapping_t _pinMapping;
};

View File

@ -25,6 +25,8 @@
#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 <ESPAsyncWebServer.h>
class WebApiClass {
@ -66,6 +68,9 @@ private:
WebApiWsLiveClass _webApiWsLive;
WebApiWsVedirectLiveClass _webApiWsVedirectLive;
WebApiVedirectClass _webApiVedirect;
WebApiHuaweiClass _webApiHuaweiClass;
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
};
extern WebApiClass WebApi;

19
include/WebApi_Huawei.h Normal file
View 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;
};

View 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;
};

View File

@ -117,3 +117,5 @@
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
#define BATTERY_ENABLED false
#define HUAWEI_ENABLED false

View File

@ -32,6 +32,7 @@ lib_deps =
nrf24/RF24 @ ^1.4.5
olikraus/U8g2 @ ^2.34.16
buelowp/sunset @ ^1.1.7
https://github.com/coryjfowler/MCP_CAN_lib
extra_scripts =
pre:auto_firmware_version.py
@ -60,6 +61,12 @@ build_flags = ${env.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:olimex_esp32_poe]
@ -102,6 +109,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/

View File

@ -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

View File

@ -146,6 +146,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");
@ -320,6 +323,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;
}

192
src/Huawei_can.cpp Normal file
View File

@ -0,0 +1,192 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2023 Malte Schmidt and others
*/
#include "Huawei_can.h"
#include "MessageOutput.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)
{
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("MCP2515 Initialized Successfully!");
}
else {
MessageOutput.println("Error Initializing MCP2515...");
}
CAN->setMode(MCP_NORMAL); // Change to normal mode to allow messages to be transmitted
pinMode(huawei_power, OUTPUT);
digitalWrite(huawei_power,HIGH);
_huawei_power = huawei_power;
}
RectifierParameters_t * HuaweiCanClass::get()
{
return &_rp;
}
unsigned long 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;
lastUpdate = millis(); // We'll only update last update on the important params
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()
{
long unsigned int rxId;
unsigned char len = 0;
unsigned char rxBuf[8];
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);
}

115
src/MqttHandleHuawei.cpp Normal file
View 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);
}
}
}

View File

@ -66,6 +66,13 @@ 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_t& PinMappingClass::get()
@ -124,6 +131,13 @@ 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;
return true;
}
}
@ -157,3 +171,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;
}

View File

@ -41,6 +41,8 @@ void WebApiClass::init()
_webApiWsLive.init(&_server);
_webApiWsVedirectLive.init(&_server);
_webApiVedirect.init(&_server);
_webApiWsHuaweiLive.init(&_server);
_webApiHuaweiClass.init(&_server);
_server.begin();
}
@ -70,6 +72,8 @@ void WebApiClass::loop()
_webApiWsLive.loop();
_webApiWsVedirectLive.loop();
_webApiVedirect.loop();
_webApiWsHuaweiLive.loop();
_webApiHuaweiClass.loop();
}
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)

255
src/WebApi_Huawei.cpp Normal file
View File

@ -0,0 +1,255 @@
// 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 "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);
HuaweiCan.setPower(config.Huawei_Enabled);
}

128
src/WebApi_ws_Huawei.cpp Normal file
View File

@ -0,0 +1,128 @@
// 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();
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);
}
}
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;
}
AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U);
JsonVariant root = response->getRoot().as<JsonVariant>();
generateJsonResponse(root);
response->setLength();
request->send(response);
}

View File

@ -185,6 +185,10 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
JsonObject vedirectObj = root.createNestedObject("vedirect");
vedirectObj[F("enabled")] = Configuration.get().Vedirect_Enabled;
JsonObject huaweiObj = root.createNestedObject("huawei");
huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled;
}
void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic)

View File

@ -8,11 +8,13 @@
#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"
@ -100,6 +102,7 @@ void setup()
MqttHandleVedirect.init();
MqttHandleHass.init();
MqttHandleVedirectHass.init();
MqttHandleHuawei.init();
MessageOutput.println(F("done"));
// Initialize WebApi
@ -162,6 +165,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 +203,8 @@ void loop()
yield();
MqttHandleVedirectHass.loop();
yield();
MqttHandleHuawei.loop();
yield();
WebApi.loop();
yield();
Display.loop();
@ -199,4 +215,6 @@ void loop()
yield();
PylontechCanReceiver.loop();
yield();
HuaweiCan.loop();
yield();
}

View 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>

View File

@ -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>

View File

@ -10,7 +10,8 @@
"DTUSettings": "DTU",
"DeviceManager": "Hardware",
"VedirectSettings": "Ve.direct",
"BatterySettings": "Battery",
"BatterySettings": "Batterie",
"AcChargerSettings": "AC Ladegerät",
"ConfigManagement": "Konfigurationsverwaltung",
"FirmwareUpgrade": "Firmware-Aktualisierung",
"DeviceReboot": "Neustart",
@ -630,5 +631,40 @@
"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"
}
}

View File

@ -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",
@ -635,5 +636,40 @@
"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"
}
}

View File

@ -10,6 +10,8 @@
"DTUSettings": "DTU",
"DeviceManager": "Périphériques",
"VedirectSettings": "Ve.direct",
"BatterySettings": "Battery",
"AcChargerSettings": "AC Charger",
"ConfigManagement": "Gestion de la configuration",
"FirmwareUpgrade": "Mise à jour du firmware",
"DeviceReboot": "Redémarrage de l'appareil",
@ -571,5 +573,40 @@
"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"
}
}

View File

@ -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',

View File

@ -0,0 +1,3 @@
export interface AcChargerConfig {
enabled: boolean;
}

View 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;
}

View File

@ -0,0 +1,7 @@
export interface HuaweiLimitConfig {
voltage: number;
voltage_valid: boolean;
current: number;
current_valid: boolean;
online: boolean;
}

View File

@ -50,9 +50,14 @@ export interface Vedirect {
enabled: boolean;
}
export interface Huawei {
enabled: boolean;
}
export interface LiveData {
inverters: Inverter[];
total: Total;
hints: Hints;
vedirect: Vedirect;
huawei: Huawei;
}

View 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>

View File

@ -114,6 +114,9 @@
</div>
</div>
<VedirectView v-show="liveData.vedirect.enabled" />
<div v-show="liveData.huawei.enabled" >
<HuaweiView/>
</div>
</BasePage>
<div class="modal" id="eventView" tabindex="-1">
@ -325,6 +328,7 @@ 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 type { DevInfoStatus } from '@/types/DevInfoStatus';
import type { EventlogItems } from '@/types/EventlogStatus';
import type { LimitConfig } from '@/types/LimitConfig';
@ -365,7 +369,8 @@ export default defineComponent({
BIconToggleOff,
BIconToggleOn,
BIconXCircleFill,
VedirectView
VedirectView,
HuaweiView
},
data() {
return {

Binary file not shown.