diff --git a/include/WebApi_gridprofile.h b/include/WebApi_gridprofile.h index cf78cf6..37a2421 100644 --- a/include/WebApi_gridprofile.h +++ b/include/WebApi_gridprofile.h @@ -10,6 +10,7 @@ public: private: void onGridProfileStatus(AsyncWebServerRequest* request); + void onGridProfileRawdata(AsyncWebServerRequest* request); AsyncWebServer* _server; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index aa456a8..df7d85b 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -5,6 +5,8 @@ #include "GridProfileParser.h" #include "../Hoymiles.h" #include +#include +#include const std::array GridProfileParser::_profileTypes = { { { 0x02, 0x00, "no data (yet)" }, @@ -16,6 +18,263 @@ const std::array GridProfileParser::_pr { 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" }, } }; +constexpr frozen::map profile_section = { + { 0x00, "Voltage (H/LVRT)" }, + { 0x10, "Frequency (H/LFRT)" }, + { 0x20, "Island Detection (ID)" }, + { 0x30, "Reconnection (RT)" }, + { 0x40, "Ramp Rates (RR)" }, + { 0x50, "Frequency Watt (FW)" }, + { 0x60, "Volt Watt (VW)" }, + { 0x70, "Active Power Control (APC)" }, + { 0x80, "Volt Var (VV)" }, + { 0x90, "Specified Power Factor (SPF)" }, + { 0xA0, "Reactive Power Control (RPC)" }, + { 0xB0, "Watt Power Factor (WPF)" }, +}; + +struct GridProfilePartialValue_t { + frozen::string Name; + frozen::string Unit; + uint8_t Dividor; +}; + +constexpr GridProfilePartialValue_t make_value(frozen::string Name, frozen::string Unit, uint8_t divisor) +{ + GridProfilePartialValue_t v = { Name, Unit, divisor }; + return v; +} + +constexpr frozen::map value_names = { + { 0x01, make_value("Nominale Voltage (NV)", "V", 10) }, + { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, + { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x04, make_value("High Voltage 1 (HV1)", "V", 10) }, + { 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x06, make_value("Low Voltage 2 (LV2)", "V", 10) }, + { 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x08, make_value("High Voltage 2 (HV2)", "V", 10) }, + { 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) }, + { 0x0b, make_value("High Voltage 3 (HV3)", "V", 10) }, + { 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 10) }, + { 0x0d, make_value("Nominal Frequency", "Hz", 100) }, + { 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) }, + { 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) }, + { 0x10, make_value("High Frequency 1 (HF1)", "Hz", 100) }, + { 0x11, make_value("HF1 Maximum Trip time (MTT)", "s", 10) }, + { 0x12, make_value("Low Frequency 2 (LF2)", "Hz", 100) }, + { 0x13, make_value("LF2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x14, make_value("High Frequency 2 (HF2)", "Hz", 100) }, + { 0x15, make_value("HF2 Maximum Trip time (MTT)", "s", 10) }, + { 0x16, make_value("ID Function Activated", "bool", 1) }, + { 0x17, make_value("Reconnect Time (RT)", "s", 10) }, + { 0x18, make_value("Reconnect High Voltage (RHV)", "V", 10) }, + { 0x19, make_value("Reconnect Low Voltage (RLV)", "V", 10) }, + { 0x1a, make_value("Reconnect High Frequency (RHF)", "Hz", 100) }, + { 0x1b, make_value("Reconnect Low Frequency (RLF)", "Hz", 100) }, + { 0x1c, make_value("Normal Ramp up Rate(RUR_NM)", "Rated%/s", 100) }, + { 0x1d, make_value("Soft Start Ramp up Rate (RUR_SS)", "Rated%/s", 100) }, + { 0x1e, make_value("FW Function Activated", "bool", 1) }, + { 0x1f, make_value("Start of Frequency Watt Droop (Fstart)", "Hz", 100) }, + { 0x20, make_value("FW Droop Slope (Kpower_Freq)", "Pn%/Hz", 10) }, + { 0x21, make_value("Recovery Ramp Rate (RRR)", "Pn%/s", 100) }, + { 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 100) }, + { 0x23, make_value("Recovery Low Frequency (RVLF)", "Hz", 100) }, + { 0x24, make_value("VW Function Activated", "bool", 1) }, + { 0x25, make_value("Start of Voltage Watt Droop (Vstart)", "V", 10) }, + { 0x26, make_value("End of Voltage Watt Droop (Vend)", "V", 10) }, + { 0x27, make_value("Droop Slope (Kpower_Volt)", "Pn%/V", 100) }, + { 0x28, make_value("APC Function Activated", "bool", 1) }, + { 0x29, make_value("Power Ramp Rate (PRR)", "Pn%/s", 100) }, + { 0x2a, make_value("VV Function Activated", "bool", 1) }, + { 0x2b, make_value("Voltage Set Point V1", "V", 10) }, + { 0x2c, make_value("Reactive Set Point Q1", "%Pn", 10) }, + { 0x2d, make_value("Voltage Set Point V2", "V", 10) }, + { 0x2e, make_value("Voltage Set Point V3", "V", 10) }, + { 0x2f, make_value("Voltage Set Point V4", "V", 10) }, + { 0x30, make_value("Reactive Set Point Q4", "%Pn", 10) }, + { 0x31, make_value("Setting Time (Tr)", "s", 10) }, + { 0x32, make_value("SPF Function Activated", "bool", 1) }, + { 0x33, make_value("Power Factor (PF)", "", 100) }, + { 0x34, make_value("RPC Function Activated", "bool", 1) }, + { 0x35, make_value("Reactive Power (VAR)", "%Sn", 1) }, + { 0x36, make_value("WPF Function Activated", "bool", 1) }, + { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, + { 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, +}; + +const std::array GridProfileParser::_profile_values = { { + // Voltage (H/LVRT) + // Version 0x00 + { 0x00, 0x00, 0x01 }, + { 0x00, 0x00, 0x02 }, + { 0x00, 0x00, 0x03 }, + { 0x00, 0x00, 0x04 }, + { 0x00, 0x00, 0x05 }, + + // Version 0x03 + { 0x00, 0x03, 0x01 }, + { 0x00, 0x03, 0x02 }, + { 0x00, 0x03, 0x03 }, + { 0x00, 0x03, 0x05 }, + { 0x00, 0x03, 0x06 }, + { 0x00, 0x03, 0x07 }, + { 0x00, 0x03, 0x08 }, + { 0x00, 0x03, 0x09 }, + + // Version 0x0a + { 0x00, 0x0a, 0x01 }, + { 0x00, 0x0a, 0x02 }, + { 0x00, 0x0a, 0x03 }, + { 0x00, 0x0a, 0x04 }, + { 0x00, 0x0a, 0x05 }, + { 0x00, 0x0a, 0x06 }, + { 0x00, 0x0a, 0x07 }, + { 0x00, 0x0a, 0x0a }, + + // Version 0x0b + { 0x00, 0x0b, 0x01 }, + { 0x00, 0x0b, 0x02 }, + { 0x00, 0x0b, 0x03 }, + { 0x00, 0x0b, 0x04 }, + { 0x00, 0x0b, 0x05 }, + { 0x00, 0x0b, 0x06 }, + { 0x00, 0x0b, 0x07 }, + { 0x00, 0x0b, 0x08 }, + { 0x00, 0x0b, 0x09 }, + { 0x00, 0x0b, 0x0a }, + + // Version 0x0c + { 0x00, 0x0c, 0x01 }, + { 0x00, 0x0c, 0x02 }, + { 0x00, 0x0c, 0x03 }, + { 0x00, 0x0c, 0x04 }, + { 0x00, 0x0c, 0x05 }, + { 0x00, 0x0c, 0x06 }, + { 0x00, 0x0c, 0x07 }, + { 0x00, 0x0c, 0x08 }, + { 0x00, 0x0c, 0x09 }, + { 0x00, 0x0c, 0x0b }, + { 0x00, 0x0c, 0x0c }, + { 0x00, 0x0c, 0x0a }, + + // Frequency (H/LFRT) + // Version 0x00 + { 0x10, 0x00, 0x0d }, + { 0x10, 0x00, 0x0e }, + { 0x10, 0x00, 0x0f }, + { 0x10, 0x00, 0x10 }, + { 0x10, 0x00, 0x11 }, + + // Version 0x03 + { 0x10, 0x03, 0x0d }, + { 0x10, 0x03, 0x0e }, + { 0x10, 0x03, 0x0f }, + { 0x10, 0x03, 0x10 }, + { 0x10, 0x03, 0x11 }, + { 0x10, 0x03, 0x12 }, + { 0x10, 0x03, 0x13 }, + { 0x10, 0x03, 0x14 }, + { 0x10, 0x03, 0x15 }, + + // Island Detection (ID) + // Version 0x00 + { 0x20, 0x00, 0x16 }, + + // Reconnection (RT) + // Version 0x03 + { 0x30, 0x03, 0x17 }, + { 0x30, 0x03, 0x18 }, + { 0x30, 0x03, 0x19 }, + { 0x30, 0x03, 0x1a }, + { 0x30, 0x03, 0x1b }, + + // Ramp Rates (RR) + // Version 0x00 + { 0x40, 0x00, 0x1c }, + { 0x40, 0x00, 0x1d }, + + // Frequency Watt (FW) + // Version 0x00 + { 0x50, 0x00, 0x1e }, + { 0x50, 0x00, 0x1f }, + { 0x50, 0x00, 0x20 }, + { 0x50, 0x00, 0x21 }, + + // Version 0x01 + { 0x50, 0x01, 0x1e }, + { 0x50, 0x01, 0x1f }, + { 0x50, 0x01, 0x20 }, + { 0x50, 0x01, 0x21 }, + { 0x50, 0x01, 0x22 }, + + // Version 0x08 + { 0x50, 0x08, 0x1e }, + { 0x50, 0x08, 0x1f }, + { 0x50, 0x08, 0x20 }, + { 0x50, 0x08, 0x21 }, + { 0x50, 0x08, 0x22 }, + { 0x50, 0x08, 0x23 }, + + // Volt Watt (VW) + // Version 0x00 + { 0x60, 0x00, 0x24 }, + { 0x60, 0x00, 0x25 }, + { 0x60, 0x00, 0x26 }, + { 0x60, 0x00, 0x27 }, + + // Version 0x04 + { 0x60, 0x04, 0x24 }, + { 0x60, 0x04, 0x25 }, + { 0x60, 0x04, 0x26 }, + { 0x60, 0x04, 0x27 }, + + // Active Power Control (APC) + // Version 0x00 + { 0x70, 0x00, 0x28 }, + + // Version 0x02 + { 0x70, 0x02, 0x28 }, + { 0x70, 0x02, 0x29 }, + + // Volt Var (VV) + // Version 0x00 + { 0x80, 0x00, 0x2a }, + { 0x80, 0x00, 0x2b }, + { 0x80, 0x00, 0x2c }, + { 0x80, 0x00, 0x2d }, + { 0x80, 0x00, 0x2e }, + { 0x80, 0x00, 0x2f }, + { 0x80, 0x00, 0x30 }, + + // Version 0x01 + { 0x80, 0x01, 0x2a }, + { 0x80, 0x01, 0x2b }, + { 0x80, 0x01, 0x2c }, + { 0x80, 0x01, 0x2d }, + { 0x80, 0x01, 0x2e }, + { 0x80, 0x01, 0x2f }, + { 0x80, 0x01, 0x30 }, + { 0x80, 0x01, 0x31 }, + + // Specified Power Factor (SPF) + // Version 0x00 + { 0x90, 0x00, 0x32 }, + { 0x90, 0x00, 0x33 }, + + // Reactive Power Control (RPC) + // Version 0x02 + { 0xa0, 0x02, 0x34 }, + { 0xa0, 0x02, 0x35 }, + + // Watt Power Factor (WPF) + // Version 0x00 + { 0xb0, 0x00, 0x36 }, + { 0xb0, 0x00, 0x37 }, + { 0xb0, 0x00, 0x38 }, +} }; + GridProfileParser::GridProfileParser() : Parser() { @@ -51,7 +310,9 @@ String GridProfileParser::getProfileName() String GridProfileParser::getProfileVersion() { char buffer[10]; + HOY_SEMAPHORE_TAKE(); snprintf(buffer, sizeof(buffer), "%d.%d.%d", (_payloadGridProfile[2] >> 4) & 0x0f, _payloadGridProfile[2] & 0x0f, _payloadGridProfile[3]); + HOY_SEMAPHORE_GIVE(); return buffer; } @@ -65,3 +326,70 @@ std::vector GridProfileParser::getRawData() HOY_SEMAPHORE_GIVE(); return ret; } + +std::list GridProfileParser::getProfile() +{ + std::list l; + + if (_gridProfileLength > 4) { + uint16_t pos = 4; + do { + uint8_t section_id = _payloadGridProfile[pos]; + uint8_t section_version = _payloadGridProfile[pos + 1]; + int8_t section_start = getSectionStart(section_id, section_version); + uint8_t section_size = getSectionSize(section_id, section_version); + pos += 2; + + GridProfileSection_t section; + try { + section.SectionName = profile_section.at(section_id).data(); + } catch (const std::out_of_range&) { + section.SectionName = "Unknown"; + break; + } + + for (uint8_t val_id = 0; val_id < section_size; val_id++) { + auto value_setting = value_names.at(_profile_values[section_start + val_id].Type); + + float value = (_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]; + value /= value_setting.Dividor; + + GridProfileItem_t v; + v.Name = value_setting.Name.data(); + v.Unit = value_setting.Unit.data(); + v.Value = value; + section.items.push_back(v); + + pos += 2; + } + + l.push_back(section); + + } while (pos < _gridProfileLength); + } + + return l; +} + +uint8_t GridProfileParser::getSectionSize(uint8_t section_id, uint8_t section_version) +{ + uint8_t count = 0; + for (auto& values : _profile_values) { + if (values.Section == section_id && values.Version == section_version) { + count++; + } + } + return count; +} + +int8_t GridProfileParser::getSectionStart(uint8_t section_id, uint8_t section_version) +{ + uint8_t count = -1; + for (auto& values : _profile_values) { + count++; + if (values.Section == section_id && values.Version == section_version) { + break; + } + } + return count; +} diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index 3065371..beacb66 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" +#include #define GRID_PROFILE_SIZE 141 #define PROFILE_TYPE_COUNT 7 +#define SECTION_VALUE_COUNT 113 typedef struct { uint8_t lIdx; @@ -11,6 +13,23 @@ typedef struct { const char* Name; } ProfileType_t; +struct GridProfileValue_t { + uint8_t Section; + uint8_t Version; + uint8_t Type; +}; + +struct GridProfileItem_t { + String Name; + String Unit; + float Value; +}; + +struct GridProfileSection_t { + String SectionName; + std::list items; +}; + class GridProfileParser : public Parser { public: GridProfileParser(); @@ -22,9 +41,15 @@ public: std::vector getRawData(); + std::list getProfile(); + private: + static uint8_t getSectionSize(uint8_t section_id, uint8_t section_version); + static int8_t getSectionStart(uint8_t section_id, uint8_t section_version); + uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; uint8_t _gridProfileLength = 0; static const std::array _profileTypes; + static const std::array _profile_values; }; \ No newline at end of file diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 599c54e..bc9ce68 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -14,6 +14,7 @@ void WebApiGridProfileClass::init(AsyncWebServer* server) _server = server; _server->on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); + _server->on("/api/gridprofile/rawdata", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileRawdata, this, _1)); } void WebApiGridProfileClass::loop() @@ -26,6 +27,50 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) return; } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); + JsonObject root = response->getRoot(); + + uint64_t serial = 0; + if (request->hasParam("inv")) { + String s = request->getParam("inv")->value(); + serial = strtoll(s.c_str(), NULL, 16); + } + + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv != nullptr) { + root["name"] = inv->GridProfile()->getProfileName(); + root["version"] = inv->GridProfile()->getProfileVersion(); + + auto jsonSections = root.createNestedArray("sections"); + auto profSections = inv->GridProfile()->getProfile(); + + for (auto &profSection : profSections) { + auto jsonSection = jsonSections.createNestedObject(); + jsonSection["name"] = profSection.SectionName; + + auto jsonItems = jsonSection.createNestedArray("items"); + + for (auto &profItem : profSection.items) { + auto jsonItem = jsonItems.createNestedObject(); + + jsonItem["n"] = profItem.Name; + jsonItem["u"] = profItem.Unit; + jsonItem["v"] = profItem.Value; + } + } + } + + response->setLength(); + request->send(response); +} + +void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); JsonObject root = response->getRoot(); @@ -42,9 +87,6 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) auto data = inv->GridProfile()->getRawData(); copyArray(&data[0], data.size(), raw); - - root["name"] = inv->GridProfile()->getProfileName(); - root["version"] = inv->GridProfile()->getProfileVersion(); } response->setLength(); diff --git a/webapp/src/components/GridProfile.vue b/webapp/src/components/GridProfile.vue index 840e897..941a7bc 100644 --- a/webapp/src/components/GridProfile.vue +++ b/webapp/src/components/GridProfile.vue @@ -19,36 +19,85 @@ - -

-  {{ $t('gridprofile.GridprofileSupport') }} -

-
- - {{ rawContent() }} - +
+
+

+ +

+
+
+ + + + + + + +
{{ value.n }} + + {{ $n(value.v, 'decimal') }} {{ value.u }} + + +
+
+
+
+
+ +
+ +
+
+

+ +

+
+
+ +

+  {{ $t('gridprofile.GridprofileSupport') }} +

+
+ + {{ rawContent() }} + +
+
+
+
+