Adding Huawei CAN interface, web-api, websocket and Mqtt extensions to access the data

This commit is contained in:
MalteSchm 2023-03-24 10:03:22 +01:00
parent 63c956af15
commit 8576034b77
13 changed files with 761 additions and 1 deletions

68
include/Huawei_can.h Normal file
View File

@ -0,0 +1,68 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <cstdint>
#include "SPI.h"
#include <mcp_can.h>
#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);
void loop();
void setValue(float in, uint8_t parameterType);
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 *hspi;
MCP_CAN *CAN;
uint8_t _huawei_irq;
};
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,11 @@ struct PinMapping_t {
uint8_t victron_rx; uint8_t victron_rx;
uint8_t battery_rx; uint8_t battery_rx;
uint8_t battery_tx; uint8_t battery_tx;
uint8_t huawei_miso;
uint8_t huawei_mosi;
uint8_t huawei_clk;
uint8_t huawei_irq;
uint8_t huawei_cs;
}; };
class PinMappingClass { class PinMappingClass {
@ -45,7 +50,8 @@ public:
bool isValidEthConfig(); bool isValidEthConfig();
bool isValidVictronConfig(); bool isValidVictronConfig();
bool isValidBatteryConfig(); bool isValidBatteryConfig();
bool isValidHuaweiConfig();
private: private:
PinMapping_t _pinMapping; PinMapping_t _pinMapping;
}; };

View File

@ -24,6 +24,8 @@
#include "WebApi_ws_live.h" #include "WebApi_ws_live.h"
#include "WebApi_ws_vedirect_live.h" #include "WebApi_ws_vedirect_live.h"
#include "WebApi_vedirect.h" #include "WebApi_vedirect.h"
#include "WebApi_ws_Huawei.h"
#include "WebApi_Huawei.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
class WebApiClass { class WebApiClass {
@ -64,6 +66,9 @@ private:
WebApiWsLiveClass _webApiWsLive; WebApiWsLiveClass _webApiWsLive;
WebApiWsVedirectLiveClass _webApiWsVedirectLive; WebApiWsVedirectLiveClass _webApiWsVedirectLive;
WebApiVedirectClass _webApiVedirect; WebApiVedirectClass _webApiVedirect;
WebApiHuaweiClass _webApiHuaweiClass;
WebApiWsHuaweiLiveClass _webApiWsHuaweiLive;
}; };
extern WebApiClass WebApi; extern WebApiClass WebApi;

16
include/WebApi_Huawei.h Normal file
View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
class WebApiHuaweiClass {
public:
void init(AsyncWebServer* server);
void loop();
private:
void onStatus(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;
};

181
src/Huawei_can.cpp Normal file
View File

@ -0,0 +1,181 @@
// 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)
{
hspi = new SPIClass(VSPI);
hspi->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(hspi, 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
}
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(*(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;
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();
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...");
}
}

104
src/MqttHandleHuawei.cpp Normal file
View File

@ -0,0 +1,104 @@
// 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 <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));
//pinMode(13, OUTPUT);
//digitalWrite(13,HIGH);
}
void MqttHandleHuaweiClass::loop()
{
if (!MqttSettings.getConnected() ) {
return;
}
const CONFIG_T& config = Configuration.get();
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("Limit Current: %f A\r\n", payload_val);
//digitalWrite(13, payload_val > 0);
}
}

View File

@ -66,6 +66,12 @@ PinMappingClass::PinMappingClass()
_pinMapping.battery_rx = PYLONTECH_PIN_RX; _pinMapping.battery_rx = PYLONTECH_PIN_RX;
_pinMapping.battery_tx = PYLONTECH_PIN_TX; _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_t& PinMappingClass::get() PinMapping_t& PinMappingClass::get()
@ -124,6 +130,12 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | PYLONTECH_PIN_RX; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | PYLONTECH_PIN_RX;
_pinMapping.battery_tx = doc[i]["battery"]["tx"] | PYLONTECH_PIN_TX; _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;
return true; return true;
} }
} }
@ -157,3 +169,12 @@ bool PinMappingClass::isValidBatteryConfig()
return _pinMapping.battery_rx > 0 return _pinMapping.battery_rx > 0
&& _pinMapping.battery_tx > 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;
}

View File

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

168
src/WebApi_Huawei.cpp Normal file
View File

@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "WebApi_Huawei.h"
#include "Huawei_can.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/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1));
}
void WebApiHuaweiClass::loop()
{
}
void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentialsReadonly(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject root = response->getRoot();
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"] = "%";
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);
}

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

@ -8,6 +8,7 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "VeDirectFrameHandler.h" #include "VeDirectFrameHandler.h"
#include "PylontechCanReceiver.h" #include "PylontechCanReceiver.h"
#include "Huawei_can.h"
#include "MqttHandleDtu.h" #include "MqttHandleDtu.h"
#include "MqttHandleHass.h" #include "MqttHandleHass.h"
#include "MqttHandleVedirectHass.h" #include "MqttHandleVedirectHass.h"
@ -158,6 +159,17 @@ void setup()
} else { } else {
MessageOutput.println(F("Invalid pin config")); 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\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs);
HuaweiCan.init(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs);
MessageOutput.println(F("done"));
} else {
MessageOutput.println(F("Invalid pin config"));
}
} }
void loop() void loop()
@ -193,4 +205,6 @@ void loop()
yield(); yield();
PylontechCanReceiver.loop(); PylontechCanReceiver.loop();
yield(); yield();
HuaweiCan.loop();
yield();
} }