diff --git a/include/Huawei_can.h b/include/Huawei_can.h new file mode 100644 index 00000000..4018d272 --- /dev/null +++ b/include/Huawei_can.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "SPI.h" +#include + +#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; diff --git a/include/MqttHandleHuawei.h b/include/MqttHandleHuawei.h new file mode 100644 index 00000000..bdb014c0 --- /dev/null +++ b/include/MqttHandleHuawei.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include + +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; \ No newline at end of file diff --git a/include/PinMapping.h b/include/PinMapping.h index 4e2900f0..87c216bd 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -33,6 +33,11 @@ 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; }; class PinMappingClass { @@ -45,7 +50,8 @@ public: bool isValidEthConfig(); bool isValidVictronConfig(); bool isValidBatteryConfig(); - + bool isValidHuaweiConfig(); + private: PinMapping_t _pinMapping; }; diff --git a/include/WebApi.h b/include/WebApi.h index 9f223402..ac094d3b 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -24,6 +24,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 class WebApiClass { @@ -64,6 +66,9 @@ private: WebApiWsLiveClass _webApiWsLive; WebApiWsVedirectLiveClass _webApiWsVedirectLive; WebApiVedirectClass _webApiVedirect; + WebApiHuaweiClass _webApiHuaweiClass; + WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; + }; extern WebApiClass WebApi; \ No newline at end of file diff --git a/include/WebApi_Huawei.h b/include/WebApi_Huawei.h new file mode 100644 index 00000000..a75b7937 --- /dev/null +++ b/include/WebApi_Huawei.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiHuaweiClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onStatus(AsyncWebServerRequest* request); + void onPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h new file mode 100644 index 00000000..9ace7e88 --- /dev/null +++ b/include/WebApi_ws_Huawei.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include +//#include + +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; +}; \ No newline at end of file diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp new file mode 100644 index 00000000..2c3136da --- /dev/null +++ b/src/Huawei_can.cpp @@ -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 +#include + +#include + +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..."); + } +} \ No newline at end of file diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp new file mode 100644 index 00000000..f6779b3b --- /dev/null +++ b/src/MqttHandleHuawei.cpp @@ -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 + +#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); + } +} \ No newline at end of file diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 248e369b..79ac3c53 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -66,6 +66,12 @@ 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_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_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; } } @@ -157,3 +169,12 @@ 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; +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index dad4db24..4f6102fe 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -40,6 +40,8 @@ void WebApiClass::init() _webApiWsLive.init(&_server); _webApiWsVedirectLive.init(&_server); _webApiVedirect.init(&_server); + _webApiWsHuaweiLive.init(&_server); + _webApiHuaweiClass.init(&_server); _server.begin(); } @@ -68,6 +70,8 @@ void WebApiClass::loop() _webApiWsLive.loop(); _webApiWsVedirectLive.loop(); _webApiVedirect.loop(); + _webApiWsHuaweiLive.loop(); + _webApiHuaweiClass.loop(); } bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp new file mode 100644 index 00000000..4b23aed5 --- /dev/null +++ b/src/WebApi_Huawei.cpp @@ -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 +#include + +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(); + 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()) { + if (root[F("voltage")].as() < minimal_voltage || root[F("voltage")].as() > 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(); + 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()) { + if (root[F("current")].as() < 0 || root[F("current")].as() > 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(); + 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); +} \ No newline at end of file diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp new file mode 100644 index 00000000..55c25b07 --- /dev/null +++ b/src/WebApi_ws_Huawei.cpp @@ -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(); + generateJsonResponse(root); + + response->setLength(); + request->send(response); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 33066ac9..3ebc1842 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "MessageOutput.h" #include "VeDirectFrameHandler.h" #include "PylontechCanReceiver.h" +#include "Huawei_can.h" #include "MqttHandleDtu.h" #include "MqttHandleHass.h" #include "MqttHandleVedirectHass.h" @@ -158,6 +159,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\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() @@ -193,4 +205,6 @@ void loop() yield(); PylontechCanReceiver.loop(); yield(); + HuaweiCan.loop(); + yield(); }