From 9ac6dd6e8d94c9ec656ed4a6946d292d1b9d99a2 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 7 Sep 2023 19:57:40 +0200 Subject: [PATCH] Feature: First very basic support to read the grid profile The parser is still missing and requires community support to collect data. --- include/WebApi.h | 2 + include/WebApi_gridprofile.h | 15 +++++ lib/Hoymiles/src/Hoymiles.cpp | 5 ++ .../src/commands/GridOnProFilePara.cpp | 40 +++++++++++++ lib/Hoymiles/src/commands/GridOnProFilePara.h | 13 +++++ lib/Hoymiles/src/commands/README.md | 1 + lib/Hoymiles/src/inverters/HM_Abstract.cpp | 25 +++++++- lib/Hoymiles/src/inverters/HM_Abstract.h | 1 + .../src/inverters/InverterAbstract.cpp | 6 ++ lib/Hoymiles/src/inverters/InverterAbstract.h | 4 ++ lib/Hoymiles/src/parser/GridProfileParser.cpp | 57 +++++++++++++++++++ lib/Hoymiles/src/parser/GridProfileParser.h | 24 ++++++++ src/WebApi.cpp | 2 + src/WebApi_gridprofile.cpp | 49 ++++++++++++++++ webapp/src/components/GridProfile.vue | 50 ++++++++++++++++ webapp/src/locales/de.json | 10 +++- webapp/src/locales/en.json | 10 +++- webapp/src/locales/fr.json | 10 +++- webapp/src/types/GridProfileStatus.ts | 3 + webapp/src/views/HomeView.vue | 56 ++++++++++++++++++ 20 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 include/WebApi_gridprofile.h create mode 100644 lib/Hoymiles/src/commands/GridOnProFilePara.cpp create mode 100644 lib/Hoymiles/src/commands/GridOnProFilePara.h create mode 100644 lib/Hoymiles/src/parser/GridProfileParser.cpp create mode 100644 lib/Hoymiles/src/parser/GridProfileParser.h create mode 100644 src/WebApi_gridprofile.cpp create mode 100644 webapp/src/components/GridProfile.vue create mode 100644 webapp/src/types/GridProfileStatus.ts diff --git a/include/WebApi.h b/include/WebApi.h index 42e7dc17..b61091de 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -7,6 +7,7 @@ #include "WebApi_dtu.h" #include "WebApi_eventlog.h" #include "WebApi_firmware.h" +#include "WebApi_gridprofile.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" #include "WebApi_maintenance.h" @@ -43,6 +44,7 @@ private: WebApiDtuClass _webApiDtu; WebApiEventlogClass _webApiEventlog; WebApiFirmwareClass _webApiFirmware; + WebApiGridProfileClass _webApiGridprofile; WebApiInverterClass _webApiInverter; WebApiLimitClass _webApiLimit; WebApiMaintenanceClass _webApiMaintenance; diff --git a/include/WebApi_gridprofile.h b/include/WebApi_gridprofile.h new file mode 100644 index 00000000..cf78cf64 --- /dev/null +++ b/include/WebApi_gridprofile.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class WebApiGridProfileClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onGridProfileStatus(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index f579106f..6c507741 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -100,6 +100,11 @@ void HoymilesClass::loop() } } + // Fetch grid profile + if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { + iv->sendGridOnProFileParaRequest(); + } + if (++inverterPos >= getNumInverters()) { inverterPos = 0; } diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp new file mode 100644 index 00000000..e9171672 --- /dev/null +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "GridOnProFilePara.h" +#include "Hoymiles.h" +#include "inverters/InverterAbstract.h" + +GridOnProFilePara::GridOnProFilePara(uint64_t target_address, uint64_t router_address, time_t time) + : MultiDataCommand(target_address, router_address) +{ + setTime(time); + setDataType(0x02); + setTimeout(500); +} + +String GridOnProFilePara::getCommandName() +{ + return "GridOnProFilePara"; +} + +bool GridOnProFilePara::handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id) +{ + // Check CRC of whole payload + if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + return false; + } + + // Move all fragments into target buffer + uint8_t offs = 0; + inverter->GridProfile()->beginAppendFragment(); + inverter->GridProfile()->clearBuffer(); + for (uint8_t i = 0; i < max_fragment_id; i++) { + inverter->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + offs += (fragment[i].len); + } + inverter->GridProfile()->endAppendFragment(); + inverter->GridProfile()->setLastUpdate(millis()); + return true; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.h b/lib/Hoymiles/src/commands/GridOnProFilePara.h new file mode 100644 index 00000000..41ee57ec --- /dev/null +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "MultiDataCommand.h" + +class GridOnProFilePara : public MultiDataCommand { +public: + explicit GridOnProFilePara(uint64_t target_address = 0, uint64_t router_address = 0, time_t time = 0); + + virtual String getCommandName(); + + virtual bool handleResponse(InverterAbstract* inverter, fragment_t fragment[], uint8_t max_fragment_id); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/README.md b/lib/Hoymiles/src/commands/README.md index 90ca62d3..8d5f3b36 100644 --- a/lib/Hoymiles/src/commands/README.md +++ b/lib/Hoymiles/src/commands/README.md @@ -8,6 +8,7 @@ * AlarmDataCommand * DevInfoAllCommand * DevInfoSimpleCommand + * GridOnProFilePara * RealTimeRunDataCommand * SystemConfigParaCommand * ParaSetCommand diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 7da96929..7997b2ea 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -8,6 +8,7 @@ #include "commands/AlarmDataCommand.h" #include "commands/DevInfoAllCommand.h" #include "commands/DevInfoSimpleCommand.h" +#include "commands/GridOnProFilePara.h" #include "commands/PowerControlCommand.h" #include "commands/RealTimeRunDataCommand.h" #include "commands/SystemConfigParaCommand.h" @@ -202,4 +203,26 @@ bool HM_Abstract::resendPowerControlRequest() return false; break; } -} \ No newline at end of file +} + +bool HM_Abstract::sendGridOnProFileParaRequest() +{ + if (!getEnablePolling()) { + return false; + } + + struct tm timeinfo; + if (!getLocalTime(&timeinfo, 5)) { + return false; + } + + time_t now; + time(&now); + + auto cmd = _radio->prepareCommand(); + cmd->setTime(now); + cmd->setTargetAddress(serial()); + _radio->enqueCommand(cmd); + + return true; +} diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index ea44c242..3a5cc637 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -15,6 +15,7 @@ public: bool sendPowerControlRequest(bool turnOn); bool sendRestartControlRequest(); bool resendPowerControlRequest(); + bool sendGridOnProFileParaRequest(); private: uint8_t _lastAlarmLogCnt = 0; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 83d128ad..4c5aa422 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -20,6 +20,7 @@ InverterAbstract::InverterAbstract(HoymilesRadio* radio, uint64_t serial) _alarmLogParser.reset(new AlarmLogParser()); _devInfoParser.reset(new DevInfoParser()); + _gridProfileParser.reset(new GridProfileParser()); _powerCommandParser.reset(new PowerCommandParser()); _statisticsParser.reset(new StatisticsParser()); _systemConfigParaParser.reset(new SystemConfigParaParser()); @@ -146,6 +147,11 @@ DevInfoParser* InverterAbstract::DevInfo() return _devInfoParser.get(); } +GridProfileParser* InverterAbstract::GridProfile() +{ + return _gridProfileParser.get(); +} + PowerCommandParser* InverterAbstract::PowerCommand() { return _powerCommandParser.get(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index d3b8bf53..e6f70f07 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -4,6 +4,7 @@ #include "../commands/ActivePowerControlCommand.h" #include "../parser/AlarmLogParser.h" #include "../parser/DevInfoParser.h" +#include "../parser/GridProfileParser.h" #include "../parser/PowerCommandParser.h" #include "../parser/StatisticsParser.h" #include "../parser/SystemConfigParaParser.h" @@ -71,11 +72,13 @@ public: virtual bool sendRestartControlRequest() = 0; virtual bool resendPowerControlRequest() = 0; virtual bool sendChangeChannelRequest(); + virtual bool sendGridOnProFileParaRequest() = 0; HoymilesRadio* getRadio(); AlarmLogParser* EventLog(); DevInfoParser* DevInfo(); + GridProfileParser* GridProfile(); PowerCommandParser* PowerCommand(); StatisticsParser* Statistics(); SystemConfigParaParser* SystemConfigPara(); @@ -102,6 +105,7 @@ private: std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; + std::unique_ptr _gridProfileParser; std::unique_ptr _powerCommandParser; std::unique_ptr _statisticsParser; std::unique_ptr _systemConfigParaParser; diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp new file mode 100644 index 00000000..ff69c3d6 --- /dev/null +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "GridProfileParser.h" +#include "../Hoymiles.h" +#include + +#define HOY_SEMAPHORE_TAKE() \ + do { \ + } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) +#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) + +GridProfileParser::GridProfileParser() + : Parser() +{ + _xSemaphore = xSemaphoreCreateMutex(); + HOY_SEMAPHORE_GIVE(); // release before first use + clearBuffer(); +} + +void GridProfileParser::clearBuffer() +{ + memset(_payloadGridProfile, 0, GRID_PROFILE_SIZE); + _gridProfileLength = 0; +} + +void GridProfileParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t len) +{ + if (offset + len > GRID_PROFILE_SIZE) { + Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) grid profile packet too large for buffer\r\n", __FILE__, __LINE__); + return; + } + memcpy(&_payloadGridProfile[offset], payload, len); + _gridProfileLength += len; +} + +void GridProfileParser::beginAppendFragment() +{ + HOY_SEMAPHORE_TAKE(); +} + +void GridProfileParser::endAppendFragment() +{ + HOY_SEMAPHORE_GIVE(); +} + +std::vector GridProfileParser::getRawData() +{ + std::vector ret; + HOY_SEMAPHORE_TAKE(); + for (uint8_t i = 0; i < GRID_PROFILE_SIZE; i++) { + ret.push_back(_payloadGridProfile[i]); + } + HOY_SEMAPHORE_GIVE(); + return ret; +} diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h new file mode 100644 index 00000000..fba7066b --- /dev/null +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include "Parser.h" +#include + +#define GRID_PROFILE_SIZE 141 + +class GridProfileParser : public Parser { +public: + GridProfileParser(); + void clearBuffer(); + void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + + void beginAppendFragment(); + void endAppendFragment(); + + std::vector getRawData(); + +private: + uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; + uint8_t _gridProfileLength = 0; + + SemaphoreHandle_t _xSemaphore; +}; \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index fca1b088..511a3845 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -23,6 +23,7 @@ void WebApiClass::init() _webApiDtu.init(&_server); _webApiEventlog.init(&_server); _webApiFirmware.init(&_server); + _webApiGridprofile.init(&_server); _webApiInverter.init(&_server); _webApiLimit.init(&_server); _webApiMaintenance.init(&_server); @@ -48,6 +49,7 @@ void WebApiClass::loop() _webApiDtu.loop(); _webApiEventlog.loop(); _webApiFirmware.loop(); + _webApiGridprofile.loop(); _webApiInverter.loop(); _webApiLimit.loop(); _webApiMaintenance.loop(); diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp new file mode 100644 index 00000000..c9d2adb8 --- /dev/null +++ b/src/WebApi_gridprofile.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_gridprofile.h" +#include "WebApi.h" +#include +#include + +void WebApiGridProfileClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); +} + +void WebApiGridProfileClass::loop() +{ +} + +void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); + 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) { + auto raw = root.createNestedArray("raw"); + auto data = inv->GridProfile()->getRawData(); + + copyArray(&data[0], data.size(), raw); + } + + response->setLength(); + request->send(response); +} \ No newline at end of file diff --git a/webapp/src/components/GridProfile.vue b/webapp/src/components/GridProfile.vue new file mode 100644 index 00000000..31a14133 --- /dev/null +++ b/webapp/src/components/GridProfile.vue @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 254be15f..49d5588f 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -126,7 +126,9 @@ "Failure": "Fehlgeschlagen", "Pending": "Ausstehend", "Ok": "Ok", - "Unknown": "Unbekannt" + "Unknown": "Unbekannt", + "ShowGridProfile": "Zeige Grid Profil", + "GridProfile": "Grid Profil" }, "eventlog": { "Start": "Begin", @@ -149,6 +151,12 @@ "HardwarePartNumber": "Hardware-Teilenummer", "HardwareVersion": "Hardware-Version" }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "GridprofileSupport": "Unterstütze die Entwicklung", + "GridprofileSupportLong": "Weitere Informationen sind hier zu finden." + }, "systeminfo": { "SystemInfo": "System Informationen", "VersionError": "Fehler beim Abrufen von Versionsinformationen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 2040e5ac..67c6dbf5 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -126,7 +126,9 @@ "Failure": "Failure", "Pending": "Pending", "Ok": "Ok", - "Unknown": "Unknown" + "Unknown": "Unknown", + "ShowGridProfile": "Show Grid Profile", + "GridProfile": "Grid Profile" }, "eventlog": { "Start": "Start", @@ -149,6 +151,12 @@ "HardwarePartNumber": "Hardware Part Number", "HardwareVersion": "Hardware Version" }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "GridprofileSupport": "Support the development", + "GridprofileSupportLong": "Please see here for further information." + }, "systeminfo": { "SystemInfo": "System Info", "VersionError": "Error fetching version information", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 0e7e9df0..7f9fb072 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -126,7 +126,9 @@ "Failure": "Échec", "Pending": "En attente", "Ok": "OK", - "Unknown": "Inconnu" + "Unknown": "Inconnu", + "ShowGridProfile": "Show Grid Profile", + "GridProfile": "Grid Profile" }, "eventlog": { "Start": "Départ", @@ -149,6 +151,12 @@ "HardwarePartNumber": "Numéro d'article matériel", "HardwareVersion": "Version du matériel" }, + "gridprofile": { + "NoInfo": "@:devinfo.NoInfo", + "NoInfoLong": "@:devinfo.NoInfoLong", + "GridprofileSupport": "Support the development", + "GridprofileSupportLong": "Please see here for further information." + }, "systeminfo": { "SystemInfo": "Informations sur le système", "VersionError": "Erreur de récupération des informations de version", diff --git a/webapp/src/types/GridProfileStatus.ts b/webapp/src/types/GridProfileStatus.ts new file mode 100644 index 00000000..5cae865c --- /dev/null +++ b/webapp/src/types/GridProfileStatus.ts @@ -0,0 +1,3 @@ +export interface GridProfileStatus { + raw: Array; +} \ No newline at end of file diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index e64361a1..70686fa1 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -78,6 +78,14 @@ +
+ +
+
+ +