diff --git a/include/Configuration.h b/include/Configuration.h index 5c10e15bb..8cb44caf8 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include #define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011900 // 0.1.24 // make sure to clean all after change @@ -54,6 +54,9 @@ struct INVERTER_CONFIG_T { bool Poll_Enable_Night; bool Command_Enable; bool Command_Enable_Night; + uint8_t ReachableThreshold; + bool ZeroRuntimeDataIfUnrechable; + bool ZeroYieldDayOnMidnight; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; @@ -72,18 +75,18 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { struct CONFIG_T { uint32_t Cfg_Version; - uint Cfg_SaveCount; + uint32_t Cfg_SaveCount; char WiFi_Ssid[WIFI_MAX_SSID_STRLEN + 1]; char WiFi_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; - byte WiFi_Ip[4]; - byte WiFi_Netmask[4]; - byte WiFi_Gateway[4]; - byte WiFi_Dns1[4]; - byte WiFi_Dns2[4]; + uint8_t WiFi_Ip[4]; + uint8_t WiFi_Netmask[4]; + uint8_t WiFi_Gateway[4]; + uint8_t WiFi_Dns1[4]; + uint8_t WiFi_Dns2[4]; bool WiFi_Dhcp; char WiFi_Hostname[WIFI_MAX_HOSTNAME_STRLEN + 1]; - uint WiFi_ApTimeout; + uint32_t WiFi_ApTimeout; char Ntp_Server[NTP_MAX_SERVER_STRLEN + 1]; char Ntp_Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; @@ -95,7 +98,7 @@ struct CONFIG_T { bool Mqtt_Enabled; char Mqtt_Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; bool Mqtt_VerboseLogging; - uint Mqtt_Port; + uint32_t Mqtt_Port; char Mqtt_Username[MQTT_MAX_USERNAME_STRLEN + 1]; char Mqtt_Password[MQTT_MAX_PASSWORD_STRLEN + 1]; char Mqtt_Topic[MQTT_MAX_TOPIC_STRLEN + 1]; @@ -104,6 +107,7 @@ struct CONFIG_T { char Mqtt_LwtValue_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; char Mqtt_LwtValue_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; uint32_t Mqtt_PublishInterval; + bool Mqtt_CleanSession; INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; diff --git a/include/Datastore.h b/include/Datastore.h index fd42174d8..6e4c03964 100644 --- a/include/Datastore.h +++ b/include/Datastore.h @@ -31,16 +31,16 @@ class DatastoreClass { float getTotalDcIrradiation(); // Amount of relevant digits for yield total - unsigned int getTotalAcYieldTotalDigits(); + uint32_t getTotalAcYieldTotalDigits(); // Amount of relevant digits for yield total - unsigned int getTotalAcYieldDayDigits(); + uint32_t getTotalAcYieldDayDigits(); // Amount of relevant digits for AC power - unsigned int getTotalAcPowerDigits(); + uint32_t getTotalAcPowerDigits(); // Amount of relevant digits for DC power - unsigned int getTotalDcPowerDigits(); + uint32_t getTotalDcPowerDigits(); // True, if at least one inverter is reachable bool getIsAtLeastOneReachable(); @@ -68,10 +68,10 @@ class DatastoreClass { float _totalDcPowerIrradiation = 0; float _totalDcIrradiationInstalled = 0; float _totalDcIrradiation = 0; - unsigned int _totalAcYieldTotalDigits = 0; - unsigned int _totalAcYieldDayDigits = 0; - unsigned int _totalAcPowerDigits = 0; - unsigned int _totalDcPowerDigits = 0; + uint32_t _totalAcYieldTotalDigits = 0; + uint32_t _totalAcYieldDayDigits = 0; + uint32_t _totalAcPowerDigits = 0; + uint32_t _totalDcPowerDigits = 0; bool _isAtLeastOneReachable = false; bool _isAtLeastOneProducing = false; bool _isAllEnabledProducing = false; diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index ee4275037..6f089cdf0 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -3,6 +3,7 @@ #include "Configuration.h" #include +#include #include class MqttHandleInverterClass { diff --git a/include/NetworkSettings.h b/include/NetworkSettings.h index 87c8dce9e..f2cbdad3f 100644 --- a/include/NetworkSettings.h +++ b/include/NetworkSettings.h @@ -63,10 +63,10 @@ class NetworkSettingsClass { void NetworkEvent(WiFiEvent_t event); bool adminEnabled = true; bool forceDisconnection = false; - int adminTimeoutCounter = 0; - int adminTimeoutCounterMax = 0; - int connectTimeoutTimer = 0; - int connectRedoTimer = 0; + uint32_t adminTimeoutCounter = 0; + uint32_t adminTimeoutCounterMax = 0; + uint32_t connectTimeoutTimer = 0; + uint32_t connectRedoTimer = 0; uint32_t lastTimerCall = 0; const byte DNS_PORT = 53; IPAddress apIp; diff --git a/include/SunPosition.h b/include/SunPosition.h index 691d42c08..c268813c9 100644 --- a/include/SunPosition.h +++ b/include/SunPosition.h @@ -22,8 +22,8 @@ class SunPositionClass { SunSet _sun; bool _isDayPeriod = true; bool _isSunsetAvailable = true; - uint _sunriseMinutes = 0; - uint _sunsetMinutes = 0; + uint32_t _sunriseMinutes = 0; + uint32_t _sunsetMinutes = 0; uint32_t _lastUpdate = 0; bool _isValidInfo = false; diff --git a/include/WebApi.h b/include/WebApi.h index d7a8ab85c..40e66e339 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -8,6 +8,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" @@ -52,6 +53,7 @@ class WebApiClass { 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 000000000..cf78cf647 --- /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/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 0cf1449b5..1e6649200 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -25,4 +25,6 @@ class WebApiWsLiveClass { uint32_t _lastInvUpdateCheck = 0; uint32_t _lastWsCleanup = 0; uint32_t _newestInverterTimestamp = 0; + + std::mutex _mutex; }; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index 051b0ba9c..c02067411 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -74,6 +74,7 @@ #define MQTT_LWT_ONLINE "online" #define MQTT_LWT_OFFLINE "offline" #define MQTT_PUBLISH_INTERVAL 5U +#define MQTT_CLEAN_SESSION true #define DTU_SERIAL 0x99978563412U #define DTU_POLL_INTERVAL 5U @@ -95,6 +96,8 @@ #define DISPLAY_CONTRAST 60U #define DISPLAY_LANGUAGE 0U +#define REACHABLE_THRESHOLD 2U + #define VEDIRECT_ENABLED false #define VEDIRECT_VERBOSE_LOGGING false #define VEDIRECT_UPDATESONLY true diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index a5f468b56..8504d87ff 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Hoymiles.h" +#include "Utils.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_2CH.h" #include "inverters/HMS_4CH.h" @@ -12,18 +13,10 @@ #include "inverters/HM_4CH.h" #include -#define HOY_SEMAPHORE_TAKE() \ - do { \ - } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) -#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - HoymilesClass Hoymiles; void HoymilesClass::init() { - _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use - _pollInterval = 0; _radioNrf.reset(new HoymilesRadio_NRF()); _radioCmt.reset(new HoymilesRadio_CMT()); @@ -41,7 +34,7 @@ void HoymilesClass::initCMT(int8_t pin_sdio, int8_t pin_clk, int8_t pin_cs, int8 void HoymilesClass::loop() { - HOY_SEMAPHORE_TAKE(); + std::lock_guard lock(_mutex); _radioNrf->loop(); _radioCmt->loop(); @@ -57,67 +50,90 @@ void HoymilesClass::loop() } if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { - _messageOutput->print("Fetch inverter: "); - _messageOutput->println(iv->serial(), HEX); - if (!iv->isReachable()) { - iv->sendChangeChannelRequest(); - } + if (iv->getEnablePolling() || iv->getEnableCommands()) { + _messageOutput->print("Fetch inverter: "); + _messageOutput->println(iv->serial(), HEX); - iv->sendStatsRequest(); + if (!iv->isReachable()) { + iv->sendChangeChannelRequest(); + } - // Fetch event log - bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; - iv->sendAlarmLogRequest(force); + iv->sendStatsRequest(); - // Fetch limit - if ((iv->SystemConfigPara()->getLastLimitRequestSuccess() == CMD_NOK) - || ((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) - && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { - _messageOutput->println("Request SystemConfigPara"); - iv->sendSystemConfigParaRequest(); - } + // Fetch event log + bool force = iv->EventLog()->getLastAlarmRequestSuccess() == CMD_NOK; + iv->sendAlarmLogRequest(force); - // Set limit if required - if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend ActivePowerControl"); - iv->resendActivePowerControlRequest(); - } + // Fetch limit + if (((millis() - iv->SystemConfigPara()->getLastUpdateRequest() > HOY_SYSTEM_CONFIG_PARA_POLL_INTERVAL) + && (millis() - iv->SystemConfigPara()->getLastUpdateCommand() > HOY_SYSTEM_CONFIG_PARA_POLL_MIN_DURATION))) { + _messageOutput->println("Request SystemConfigPara"); + iv->sendSystemConfigParaRequest(); + } - // Set power status if required - if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { - _messageOutput->println("Resend PowerCommand"); - iv->resendPowerControlRequest(); - } + // Set limit if required + if (iv->SystemConfigPara()->getLastLimitCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend ActivePowerControl"); + iv->resendActivePowerControlRequest(); + } - // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0) { - bool invalidDevInfo = !iv->DevInfo()->containsValidData() - && iv->DevInfo()->getLastUpdateAll() > 0 - && iv->DevInfo()->getLastUpdateSimple() > 0; + // Set power status if required + if (iv->PowerCommand()->getLastPowerCommandSuccess() == CMD_NOK) { + _messageOutput->println("Resend PowerCommand"); + iv->resendPowerControlRequest(); + } - if (invalidDevInfo) { - _messageOutput->println("DevInfo: No Valid Data"); + // Fetch dev info (but first fetch stats) + if (iv->Statistics()->getLastUpdate() > 0) { + bool invalidDevInfo = !iv->DevInfo()->containsValidData() + && iv->DevInfo()->getLastUpdateAll() > 0 + && iv->DevInfo()->getLastUpdateSimple() > 0; + + if (invalidDevInfo) { + _messageOutput->println("DevInfo: No Valid Data"); + } + + if ((iv->DevInfo()->getLastUpdateAll() == 0) + || (iv->DevInfo()->getLastUpdateSimple() == 0) + || invalidDevInfo) { + _messageOutput->println("Request device info"); + iv->sendDevInfoRequest(); + } } - if ((iv->DevInfo()->getLastUpdateAll() == 0) - || (iv->DevInfo()->getLastUpdateSimple() == 0) - || invalidDevInfo) { - _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(); + // Fetch grid profile + if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { + iv->sendGridOnProFileParaRequest(); } + + _lastPoll = millis(); } if (++inverterPos >= getNumInverters()) { inverterPos = 0; } + } - _lastPoll = millis(); + // Perform housekeeping of all inverters on day change + int8_t currentWeekDay = Utils::getWeekDay(); + static int8_t lastWeekDay = -1; + if (lastWeekDay == -1) { + lastWeekDay = currentWeekDay; + } else { + if (currentWeekDay != lastWeekDay) { + + for (auto& inv : _inverters) { + if (inv->getZeroYieldDayOnMidnight()) { + inv->Statistics()->zeroDailyData(); + } + } + + lastWeekDay = currentWeekDay; + } } } } - - HOY_SEMAPHORE_GIVE(); } std::shared_ptr HoymilesClass::addInverter(const char* name, uint64_t serial) @@ -195,9 +211,8 @@ void HoymilesClass::removeInverterBySerial(uint64_t serial) { for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { - HOY_SEMAPHORE_TAKE(); + std::lock_guard lock(_mutex); _inverters.erase(_inverters.begin() + i); - HOY_SEMAPHORE_GIVE(); return; } } diff --git a/lib/Hoymiles/src/Hoymiles.h b/lib/Hoymiles/src/Hoymiles.h index c6300e4f5..7975f1ba1 100644 --- a/lib/Hoymiles/src/Hoymiles.h +++ b/lib/Hoymiles/src/Hoymiles.h @@ -45,7 +45,7 @@ class HoymilesClass { std::unique_ptr _radioNrf; std::unique_ptr _radioCmt; - SemaphoreHandle_t _xSemaphore; + std::mutex _mutex; uint32_t _pollInterval = 0; bool _verboseLogging = true; diff --git a/lib/Hoymiles/src/Utils.cpp b/lib/Hoymiles/src/Utils.cpp new file mode 100644 index 000000000..138e32a16 --- /dev/null +++ b/lib/Hoymiles/src/Utils.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "Utils.h" +#include + +uint8_t Utils::getWeekDay() +{ + time_t raw; + struct tm info; + time(&raw); + localtime_r(&raw, &info); + return info.tm_mday; +} \ No newline at end of file diff --git a/lib/Hoymiles/src/Utils.h b/lib/Hoymiles/src/Utils.h new file mode 100644 index 000000000..157dd75ce --- /dev/null +++ b/lib/Hoymiles/src/Utils.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class Utils { +public: + static uint8_t getWeekDay(); +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp new file mode 100644 index 000000000..e9171672d --- /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 000000000..41ee57ece --- /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 90ca62d39..8d5f3b36f 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/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 3f0aed36b..e5ece4092 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -55,4 +55,9 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract* inverter, fragment void RealTimeRunDataCommand::gotTimeout(InverterAbstract* inverter) { inverter->Statistics()->incrementRxFailureCount(); + + if (inverter->getZeroValuesIfUnreachable() && !inverter->isReachable()) { + Hoymiles.getMessageOutput()->println("Set runtime data to zero"); + inverter->Statistics()->zeroRuntimeData(); + } } \ No newline at end of file diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index ef2854690..5e238a59b 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "SystemConfigParaCommand.h" +#include "Hoymiles.h" #include "inverters/InverterAbstract.h" SystemConfigParaCommand::SystemConfigParaCommand(uint64_t target_address, uint64_t router_address, time_t time) @@ -25,6 +26,18 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract* inverter, fragmen return false; } + // Check if at least all required bytes are received + // In case of low power in the inverter it occours that some incomplete fragments + // with a valid CRC are received. + uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); + uint8_t expectedSize = inverter->SystemConfigPara()->getExpectedByteCount(); + if (fragmentsSize < expectedSize) { + Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", + getCommandName().c_str(), fragmentsSize, expectedSize); + + return false; + } + // Move all fragments into target buffer uint8_t offs = 0; inverter->SystemConfigPara()->beginAppendFragment(); diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index da6437c56..10d936791 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" @@ -210,4 +211,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 ea44c2427..3a5cc637b 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -15,6 +15,7 @@ class HM_Abstract : public InverterAbstract { 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 2383f5fcd..4c5aa422d 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()); @@ -73,7 +74,7 @@ bool InverterAbstract::isProducing() bool InverterAbstract::isReachable() { - return _enablePolling && Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT; + return _enablePolling && Statistics()->getRxFailureCount() <= _reachableThreshold; } void InverterAbstract::setEnablePolling(bool enabled) @@ -96,6 +97,36 @@ bool InverterAbstract::getEnableCommands() return _enableCommands; } +void InverterAbstract::setReachableThreshold(uint8_t threshold) +{ + _reachableThreshold = threshold; +} + +uint8_t InverterAbstract::getReachableThreshold() +{ + return _reachableThreshold; +} + +void InverterAbstract::setZeroValuesIfUnreachable(bool enabled) +{ + _zeroValuesIfUnreachable = enabled; +} + +bool InverterAbstract::getZeroValuesIfUnreachable() +{ + return _zeroValuesIfUnreachable; +} + +void InverterAbstract::setZeroYieldDayOnMidnight(bool enabled) +{ + _zeroYieldDayOnMidnight = enabled; +} + +bool InverterAbstract::getZeroYieldDayOnMidnight() +{ + return _zeroYieldDayOnMidnight; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; @@ -116,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 e12211d38..e6f70f070 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" @@ -24,7 +25,6 @@ enum { }; #define MAX_RF_FRAGMENT_COUNT 13 -#define MAX_ONLINE_FAILURE_COUNT 2 class CommandAbstract; @@ -49,6 +49,15 @@ class InverterAbstract { void setEnableCommands(bool enabled); bool getEnableCommands(); + void setReachableThreshold(uint8_t threshold); + uint8_t getReachableThreshold(); + + void setZeroValuesIfUnreachable(bool enabled); + bool getZeroValuesIfUnreachable(); + + void setZeroYieldDayOnMidnight(bool enabled); + bool getZeroYieldDayOnMidnight(); + void clearRxFragmentBuffer(); void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); @@ -63,11 +72,13 @@ class InverterAbstract { 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(); @@ -87,8 +98,14 @@ class InverterAbstract { bool _enablePolling = true; bool _enableCommands = true; + uint8_t _reachableThreshold = 3; + + bool _zeroValuesIfUnreachable = false; + bool _zeroYieldDayOnMidnight = false; + 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/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 10ed30528..2a6527ac3 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -86,16 +86,9 @@ const std::array AlarmLogParser::_alarmMe { AlarmMessageType_t::ALL, 9000, "Microinverter is suspected of being stolen" }, } }; -#define HOY_SEMAPHORE_TAKE() \ - do { \ - } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) -#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - AlarmLogParser::AlarmLogParser() : Parser() { - _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use clearBuffer(); } @@ -115,16 +108,6 @@ void AlarmLogParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t le _alarmLogLength += len; } -void AlarmLogParser::beginAppendFragment() -{ - HOY_SEMAPHORE_TAKE(); -} - -void AlarmLogParser::endAppendFragment() -{ - HOY_SEMAPHORE_GIVE(); -} - uint8_t AlarmLogParser::getEntryCount() { if (_alarmLogLength < 2) { diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index 5c37589df..a2c6b348f 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include #include #include @@ -34,8 +33,6 @@ class AlarmLogParser : public Parser { AlarmLogParser(); void clearBuffer(); void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); - void beginAppendFragment(); - void endAppendFragment(); uint8_t getEntryCount(); void getLogEntry(uint8_t entryId, AlarmLogEntry_t* entry); @@ -56,6 +53,4 @@ class AlarmLogParser : public Parser { AlarmMessageType_t _messageType = AlarmMessageType_t::ALL; static const std::array _alarmMessages; - - SemaphoreHandle_t _xSemaphore; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index eab3a303e..978af6001 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "DevInfoParser.h" #include "../Hoymiles.h" @@ -46,16 +46,9 @@ const devInfo_t devInfo[] = { { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250" } // 01 }; -#define HOY_SEMAPHORE_TAKE() \ - do { \ - } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) -#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - DevInfoParser::DevInfoParser() : Parser() { - _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use clearBufferSimple(); clearBufferAll(); } @@ -92,16 +85,6 @@ void DevInfoParser::appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8 _devInfoSimpleLength += len; } -void DevInfoParser::beginAppendFragment() -{ - HOY_SEMAPHORE_TAKE(); -} - -void DevInfoParser::endAppendFragment() -{ - HOY_SEMAPHORE_GIVE(); -} - uint32_t DevInfoParser::getLastUpdateAll() { return _lastUpdateAll; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index b18bc8a11..838ba1102 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include #define DEV_INFO_SIZE 20 @@ -14,9 +13,6 @@ class DevInfoParser : public Parser { void clearBufferSimple(); void appendFragmentSimple(uint8_t offset, uint8_t* payload, uint8_t len); - void beginAppendFragment(); - void endAppendFragment(); - uint32_t getLastUpdateAll(); void setLastUpdateAll(uint32_t lastUpdate); @@ -47,6 +43,4 @@ class DevInfoParser : public Parser { uint8_t _payloadDevInfoSimple[DEV_INFO_SIZE] = {}; uint8_t _devInfoSimpleLength = 0; - - SemaphoreHandle_t _xSemaphore; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp new file mode 100644 index 000000000..35f7689d5 --- /dev/null +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "GridProfileParser.h" +#include "../Hoymiles.h" +#include + +GridProfileParser::GridProfileParser() + : Parser() +{ + 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; +} + +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 000000000..c2af52f87 --- /dev/null +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include "Parser.h" + +#define GRID_PROFILE_SIZE 141 + +class GridProfileParser : public Parser { +public: + GridProfileParser(); + void clearBuffer(); + void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); + + std::vector getRawData(); + +private: + uint8_t _payloadGridProfile[GRID_PROFILE_SIZE] = {}; + uint8_t _gridProfileLength = 0; +}; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/Parser.cpp b/lib/Hoymiles/src/parser/Parser.cpp index 94c694c0b..b8e5e0e56 100644 --- a/lib/Hoymiles/src/parser/Parser.cpp +++ b/lib/Hoymiles/src/parser/Parser.cpp @@ -1,9 +1,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "Parser.h" +Parser::Parser() +{ + _xSemaphore = xSemaphoreCreateMutex(); + HOY_SEMAPHORE_GIVE(); // release before first use +} + uint32_t Parser::getLastUpdate() { return _lastUpdate; @@ -12,4 +18,14 @@ uint32_t Parser::getLastUpdate() void Parser::setLastUpdate(uint32_t lastUpdate) { _lastUpdate = lastUpdate; +} + +void Parser::beginAppendFragment() +{ + HOY_SEMAPHORE_TAKE(); +} + +void Parser::endAppendFragment() +{ + HOY_SEMAPHORE_GIVE(); } \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/Parser.h b/lib/Hoymiles/src/parser/Parser.h index ccb486bda..5d6df75df 100644 --- a/lib/Hoymiles/src/parser/Parser.h +++ b/lib/Hoymiles/src/parser/Parser.h @@ -1,7 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include +#define HOY_SEMAPHORE_TAKE() \ + do { \ + } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) +#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) + typedef enum { CMD_OK, CMD_NOK, @@ -10,9 +16,16 @@ typedef enum { class Parser { public: + Parser(); uint32_t getLastUpdate(); void setLastUpdate(uint32_t lastUpdate); + void beginAppendFragment(); + void endAppendFragment(); + +protected: + SemaphoreHandle_t _xSemaphore; + private: uint32_t _lastUpdate = 0; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.cpp b/lib/Hoymiles/src/parser/PowerCommandParser.cpp index 0ba3a8ae6..d698dad8d 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.cpp +++ b/lib/Hoymiles/src/parser/PowerCommandParser.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "PowerCommandParser.h" diff --git a/lib/Hoymiles/src/parser/PowerCommandParser.h b/lib/Hoymiles/src/parser/PowerCommandParser.h index 6aa042f30..e005812e6 100644 --- a/lib/Hoymiles/src/parser/PowerCommandParser.h +++ b/lib/Hoymiles/src/parser/PowerCommandParser.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include class PowerCommandParser : public Parser { public: diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index ac61be81b..71c1ebbd2 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -1,15 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "StatisticsParser.h" #include "../Hoymiles.h" -#define HOY_SEMAPHORE_TAKE() \ - do { \ - } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) -#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); static float calcUdcCh(StatisticsParser* iv, uint8_t arg0); @@ -33,11 +28,35 @@ const calcFunc_t calcFunctions[] = { { CALC_IRR_CH, &calcIrradiation } }; +const FieldId_t runtimeFields[] = { + FLD_UDC, + FLD_IDC, + FLD_PDC, + FLD_UAC, + FLD_IAC, + FLD_PAC, + FLD_F, + FLD_T, + FLD_PF, + FLD_Q, + FLD_UAC_1N, + FLD_UAC_2N, + FLD_UAC_3N, + FLD_UAC_12, + FLD_UAC_23, + FLD_UAC_31, + FLD_IAC_1, + FLD_IAC_2, + FLD_IAC_3, +}; + +const FieldId_t dailyProductionFields[] = { + FLD_YD, +}; + StatisticsParser::StatisticsParser() : Parser() { - _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use clearBuffer(); } @@ -75,16 +94,6 @@ void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _statisticLength += len; } -void StatisticsParser::beginAppendFragment() -{ - HOY_SEMAPHORE_TAKE(); -} - -void StatisticsParser::endAppendFragment() -{ - HOY_SEMAPHORE_GIVE(); -} - const byteAssign_t* StatisticsParser::getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { for (uint8_t i = 0; i < _byteAssignmentSize; i++) { @@ -150,6 +159,47 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } +bool StatisticsParser::setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value) +{ + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + + if (pos == NULL) { + return false; + } + + uint8_t ptr = pos->start + pos->num - 1; + uint8_t end = pos->start; + uint16_t div = pos->div; + + if (CMD_CALC == div) { + return false; + } + + if (setting != NULL) { + value -= setting->offset; + } + value *= static_cast(div); + + uint32_t val = 0; + if (pos->isSigned && pos->num == 2) { + val = static_cast(static_cast(value)); + } else if (pos->isSigned && pos->num == 4) { + val = static_cast(static_cast(value)); + } else { + val = static_cast(value); + } + + HOY_SEMAPHORE_TAKE(); + do { + _payloadStatistic[ptr] = val; + val >>= 8; + } while (--ptr >= end); + HOY_SEMAPHORE_GIVE(); + + return true; +} + String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { return String( @@ -253,6 +303,47 @@ uint32_t StatisticsParser::getRxFailureCount() return _rxFailureCount; } +void StatisticsParser::zeroRuntimeData() +{ + zeroFields(runtimeFields); +} + +void StatisticsParser::zeroDailyData() +{ + zeroFields(dailyProductionFields); +} + +void StatisticsParser::setLastUpdate(uint32_t lastUpdate) +{ + Parser::setLastUpdate(lastUpdate); + setLastUpdateFromInternal(lastUpdate); +} + +uint32_t StatisticsParser::getLastUpdateFromInternal() +{ + return _lastUpdateFromInternal; +} + +void StatisticsParser::setLastUpdateFromInternal(uint32_t lastUpdate) +{ + _lastUpdateFromInternal = lastUpdate; +} + +void StatisticsParser::zeroFields(const FieldId_t* fields) +{ + // Loop all channels + for (auto& t : getChannelTypes()) { + for (auto& c : getChannelsByType(t)) { + for (uint8_t i = 0; i < (sizeof(runtimeFields) / sizeof(runtimeFields[0])); i++) { + if (hasChannelFieldValue(t, c, fields[i])) { + setChannelFieldValue(t, c, fields[i], 0); + } + } + } + } + setLastUpdateFromInternal(millis()); +} + static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) { float yield = 0; diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 13d7d4f47..da291004f 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include #include #include @@ -107,8 +106,6 @@ class StatisticsParser : public Parser { StatisticsParser(); void clearBuffer(); void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); - void beginAppendFragment(); - void endAppendFragment(); void setByteAssignment(const byteAssign_t* byteAssignment, uint8_t size); @@ -125,6 +122,8 @@ class StatisticsParser : public Parser { const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + bool setChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float value); + float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); @@ -139,7 +138,19 @@ class StatisticsParser : public Parser { void incrementRxFailureCount(); uint32_t getRxFailureCount(); + void zeroRuntimeData(); + void zeroDailyData(); + + // Update time when new data from the inverter is received + void setLastUpdate(uint32_t lastUpdate); + + // Update time when internal data structure changes (from inverter and by internal manipulation) + uint32_t getLastUpdateFromInternal(); + void setLastUpdateFromInternal(uint32_t lastUpdate); + private: + void zeroFields(const FieldId_t* fields); + uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {}; uint8_t _statisticLength = 0; uint16_t _stringMaxPower[CH_CNT]; @@ -150,6 +161,5 @@ class StatisticsParser : public Parser { std::list _fieldSettings; uint32_t _rxFailureCount = 0; - - SemaphoreHandle_t _xSemaphore; + uint32_t _lastUpdateFromInternal = 0; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index 2756e1ec8..d1ed30b63 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -1,21 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "SystemConfigParaParser.h" #include "../Hoymiles.h" #include -#define HOY_SEMAPHORE_TAKE() \ - do { \ - } while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS) -#define HOY_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore) - SystemConfigParaParser::SystemConfigParaParser() : Parser() { - _xSemaphore = xSemaphoreCreateMutex(); - HOY_SEMAPHORE_GIVE(); // release before first use clearBuffer(); } @@ -35,16 +28,6 @@ void SystemConfigParaParser::appendFragment(uint8_t offset, uint8_t* payload, ui _payloadLength += len; } -void SystemConfigParaParser::beginAppendFragment() -{ - HOY_SEMAPHORE_TAKE(); -} - -void SystemConfigParaParser::endAppendFragment() -{ - HOY_SEMAPHORE_GIVE(); -} - float SystemConfigParaParser::getLimitPercent() { HOY_SEMAPHORE_TAKE(); @@ -101,4 +84,9 @@ void SystemConfigParaParser::setLastUpdateRequest(uint32_t lastUpdate) { _lastUpdateRequest = lastUpdate; setLastUpdate(lastUpdate); -} \ No newline at end of file +} + +uint8_t SystemConfigParaParser::getExpectedByteCount() +{ + return SYSTEM_CONFIG_PARA_SIZE; +} diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.h b/lib/Hoymiles/src/parser/SystemConfigParaParser.h index 4ec73817c..300a81822 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.h +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "Parser.h" -#include #define SYSTEM_CONFIG_PARA_SIZE 16 @@ -10,8 +9,6 @@ class SystemConfigParaParser : public Parser { SystemConfigParaParser(); void clearBuffer(); void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); - void beginAppendFragment(); - void endAppendFragment(); float getLimitPercent(); void setLimitPercent(float value); @@ -26,6 +23,9 @@ class SystemConfigParaParser : public Parser { uint32_t getLastUpdateRequest(); void setLastUpdateRequest(uint32_t lastUpdate); + // Returns 1 based amount of expected bytes of data + uint8_t getExpectedByteCount(); + private: uint8_t _payload[SYSTEM_CONFIG_PARA_SIZE]; uint8_t _payloadLength; @@ -35,6 +35,4 @@ class SystemConfigParaParser : public Parser { uint32_t _lastUpdateCommand = 0; uint32_t _lastUpdateRequest = 0; - - SemaphoreHandle_t _xSemaphore; }; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 0c75747e8..5ba51da5e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -33,7 +33,7 @@ build_unflags = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer bblanchon/ArduinoJson @ ^6.21.3 - https://github.com/bertmelis/espMqttClient.git#v1.4.4 + https://github.com/bertmelis/espMqttClient.git#v1.4.5 nrf24/RF24 @ ^1.4.7 olikraus/U8g2 @ ^2.35.4 buelowp/sunset @ ^1.1.7 diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index fb1d2f2de..81939e7f7 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -180,12 +180,10 @@ void JkBmsBatteryStats::mqttPublish() const void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) { - _dataPoints.updateFrom(dp); - using Label = JkBms::DataPointLabel; _manufacturer = "JKBMS"; - auto oProductId = _dataPoints.get(); + auto oProductId = dp.get(); if (oProductId.has_value()) { _manufacturer = oProductId->c_str(); auto pos = oProductId->rfind("JK"); @@ -194,12 +192,14 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) } } - auto oSoCValue = _dataPoints.get(); + auto oSoCValue = dp.get(); if (oSoCValue.has_value()) { _SoC = *oSoCValue; - auto oSoCDataPoint = _dataPoints.getDataPointFor(); + auto oSoCDataPoint = dp.getDataPointFor(); _lastUpdateSoC = oSoCDataPoint->getTimestamp(); } + _dataPoints.updateFrom(dp); + _lastUpdate = millis(); } diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 909e64dd8..74a126152 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -59,6 +59,7 @@ bool ConfigurationClass::write() mqtt["topic"] = config.Mqtt_Topic; mqtt["retain"] = config.Mqtt_Retain; mqtt["publish_interval"] = config.Mqtt_PublishInterval; + mqtt["clean_session"] = config.Mqtt_CleanSession; JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); mqtt_lwt["topic"] = config.Mqtt_LwtTopic; @@ -111,6 +112,9 @@ bool ConfigurationClass::write() inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; inv["command_enable"] = config.Inverter[i].Command_Enable; inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; + inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; + inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; + inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -278,6 +282,7 @@ bool ConfigurationClass::read() strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; + config.Mqtt_CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION; JsonObject mqtt_lwt = mqtt["lwt"]; strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); @@ -331,6 +336,9 @@ bool ConfigurationClass::read() config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true; config.Inverter[i].Command_Enable = inv["command_enable"] | true; config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; + config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD; + config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false; + config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false; JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { diff --git a/src/Datastore.cpp b/src/Datastore.cpp index 0afe2c767..4ff67b803 100644 --- a/src/Datastore.cpp +++ b/src/Datastore.cpp @@ -151,25 +151,25 @@ float DatastoreClass::getTotalDcIrradiation() return _totalDcIrradiation; } -unsigned int DatastoreClass::getTotalAcYieldTotalDigits() +uint32_t DatastoreClass::getTotalAcYieldTotalDigits() { std::lock_guard lock(_mutex); return _totalAcYieldTotalDigits; } -unsigned int DatastoreClass::getTotalAcYieldDayDigits() +uint32_t DatastoreClass::getTotalAcYieldDayDigits() { std::lock_guard lock(_mutex); return _totalAcYieldDayDigits; } -unsigned int DatastoreClass::getTotalAcPowerDigits() +uint32_t DatastoreClass::getTotalAcPowerDigits() { std::lock_guard lock(_mutex); return _totalAcPowerDigits; } -unsigned int DatastoreClass::getTotalDcPowerDigits() +uint32_t DatastoreClass::getTotalDcPowerDigits() { std::lock_guard lock(_mutex); return _totalDcPowerDigits; diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 10c6ca2e2..6fb1fd1c3 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -74,6 +74,9 @@ void InverterSettingsClass::init() config.Inverter[i].Serial); if (inv != nullptr) { + inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); + inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); + inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index a2f38f0ea..9578daf55 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -134,7 +134,7 @@ void MqttHandleHassClass::publishField(std::shared_ptr inv, Ch createDeviceInfo(deviceObj, inv); if (Configuration.get().Mqtt_Hass_Expire) { - root["exp_aft"] = Hoymiles.getNumInverters() * Configuration.get().Mqtt_PublishInterval * 3; + root["exp_aft"] = Hoymiles.getNumInverters() * max(Hoymiles.PollInterval(), Configuration.get().Mqtt_PublishInterval) * inv->getReachableThreshold(); } if (devCls != 0) { root["dev_cla"] = devCls; diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index c9621e33c..9048e06fb 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -14,6 +14,8 @@ #define TOPIC_SUB_POWER "power" #define TOPIC_SUB_RESTART "restart" +#define PUBLISH_MAX_INTERVAL 60000 + MqttHandleInverterClass MqttHandleInverter; void MqttHandleInverterClass::init() @@ -91,9 +93,9 @@ void MqttHandleInverterClass::loop() MqttSettings.publish(subtopic + "/status/last_update", String(0)); } - uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); - if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { - _lastPublishStats[i] = lastUpdate; + uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (inv->Statistics()->getLastUpdate() > 0 && (lastUpdateInternal != _lastPublishStats[i])) { + _lastPublishStats[i] = lastUpdateInternal; // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index befa930f1..b7ef76600 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -130,6 +130,7 @@ void MqttSettingsClass::performConnect() } static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); static_cast(mqttClient)->setClientId(clientId.c_str()); + static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); @@ -138,6 +139,7 @@ void MqttSettingsClass::performConnect() static_cast(mqttClient)->setCredentials(config.Mqtt_Username, config.Mqtt_Password); static_cast(mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt_Retain, config.Mqtt_LwtValue_Offline); static_cast(mqttClient)->setClientId(clientId.c_str()); + static_cast(mqttClient)->setCleanSession(config.Mqtt_CleanSession); static_cast(mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); static_cast(mqttClient)->onDisconnect(std::bind(&MqttSettingsClass::onMqttDisconnect, this, _1)); static_cast(mqttClient)->onMessage(std::bind(&MqttSettingsClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); diff --git a/src/NtpSettings.cpp b/src/NtpSettings.cpp index 4acd11690..ce043384d 100644 --- a/src/NtpSettings.cpp +++ b/src/NtpSettings.cpp @@ -1,9 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "NtpSettings.h" #include "Configuration.h" +#include #include NtpSettingsClass::NtpSettingsClass() diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp index 7ab453f0b..a32920ec1 100644 --- a/src/SunPosition.cpp +++ b/src/SunPosition.cpp @@ -5,6 +5,7 @@ #include "SunPosition.h" #include "Configuration.h" #include "Utils.h" +#include SunPositionClass SunPosition; @@ -83,7 +84,7 @@ void SunPositionClass::updateSunData() _sunriseMinutes = static_cast(sunriseRaw); _sunsetMinutes = static_cast(sunsetRaw); - uint minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; + uint32_t minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; _isDayPeriod = (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); _isSunsetAvailable = true; diff --git a/src/Utils.cpp b/src/Utils.cpp index 2e59d856e..893509612 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -10,7 +10,7 @@ uint32_t Utils::getChipId() { uint32_t chipId = 0; - for (int i = 0; i < 17; i += 8) { + for (uint8_t i = 0; i < 17; i += 8) { chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; } return chipId; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 9ae6fb534..82cd140ad 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -24,6 +24,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); @@ -57,6 +58,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 000000000..c9d2adb8d --- /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/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index c6e692b01..d5ea9b45a 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -7,6 +7,7 @@ #include "MqttHandleHass.h" #include "WebApi.h" #include "WebApi_errors.h" +#include "defaults.h" #include "helper.h" #include #include @@ -57,6 +58,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; obj["command_enable"] = config.Inverter[i].Command_Enable; obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night; + obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; + obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; + obj["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -281,6 +285,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Poll_Enable_Night = root["poll_enable_night"] | true; inverter.Command_Enable = root["command_enable"] | true; inverter.Command_Enable_Night = root["command_enable_night"] | true; + inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; + inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; + inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; arrayCount++; } @@ -311,6 +318,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (inv != nullptr) { inv->setEnablePolling(inverter.Poll_Enable); inv->setEnableCommands(inverter.Command_Enable); + inv->setReachableThreshold(inverter.ReachableThreshold); + inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); + inv->setZeroYieldDayOnMidnight(inverter.ZeroYieldDayOnMidnight); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index e9830a4f8..99de0b1b6 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -54,6 +54,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root["mqtt_client_cert_info"] = getTlsCertInfo(config.Mqtt_ClientCert); root["mqtt_lwt_topic"] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; + root["mqtt_clean_session"] = config.Mqtt_CleanSession; root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; @@ -91,6 +92,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_lwt_online"] = config.Mqtt_LwtValue_Online; root["mqtt_lwt_offline"] = config.Mqtt_LwtValue_Offline; root["mqtt_publish_interval"] = config.Mqtt_PublishInterval; + root["mqtt_clean_session"] = config.Mqtt_CleanSession; root["mqtt_hass_enabled"] = config.Mqtt_Hass_Enabled; root["mqtt_hass_expire"] = config.Mqtt_Hass_Expire; root["mqtt_hass_retain"] = config.Mqtt_Hass_Retain; @@ -156,6 +158,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline") && root.containsKey("mqtt_publish_interval") + && root.containsKey("mqtt_clean_session") && root.containsKey("mqtt_hass_enabled") && root.containsKey("mqtt_hass_expire") && root.containsKey("mqtt_hass_retain") @@ -321,6 +324,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) strlcpy(config.Mqtt_LwtValue_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt_LwtValue_Online)); strlcpy(config.Mqtt_LwtValue_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt_LwtValue_Offline)); config.Mqtt_PublishInterval = root["mqtt_publish_interval"].as(); + config.Mqtt_CleanSession = root["mqtt_clean_session"].as(); config.Mqtt_Hass_Enabled = root["mqtt_hass_enabled"].as(); config.Mqtt_Hass_Expire = root["mqtt_hass_expire"].as(); config.Mqtt_Hass_Retain = root["mqtt_hass_retain"].as(); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 154e5e88b..42bd41d67 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -66,6 +66,11 @@ void WebApiWsLiveClass::loop() if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { try { + std::lock_guard lock(_mutex); + DynamicJsonDocument root(4096 * INV_MAX_COUNT); + JsonVariant var = root; + generateJsonResponse(var); + String buffer; // free JsonDocument as soon as possible { @@ -152,6 +157,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) addField(chanTypeObj, i, inv, t, c, FLD_EFF); if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { addField(chanTypeObj, i, inv, t, c, FLD_IRR); + chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); } } } @@ -231,13 +237,9 @@ void WebApiWsLiveClass::addTotalField(JsonObject& root, String name, float value void WebApiWsLiveClass::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()); - MessageOutput.println(str); + MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id()); } else if (type == WS_EVT_DISCONNECT) { - char str[64]; - snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); - MessageOutput.println(str); + MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id()); } } @@ -248,7 +250,8 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } try { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); + std::lock_guard lock(_mutex); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096 * INV_MAX_COUNT); JsonVariant root = response->getRoot(); generateJsonResponse(root); diff --git a/webapp/package.json b/webapp/package.json index 31f237aa7..548f90520 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,29 +18,29 @@ "sortablejs": "^1.15.0", "spark-md5": "^3.0.2", "vue": "^3.3.4", - "vue-i18n": "^9.2.2", + "vue-i18n": "^9.4.0", "vue-router": "^4.2.4" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^0.12.3", + "@intlify/unplugin-vue-i18n": "^1.2.0", "@rushstack/eslint-patch": "^1.3.3", - "@tsconfig/node18": "^18.2.1", + "@tsconfig/node18": "^18.2.2", "@types/bootstrap": "^5.2.6", - "@types/node": "^20.5.7", + "@types/node": "^20.6.0", "@types/sortablejs": "^1.15.2", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^4.3.3", - "@vue/eslint-config-typescript": "^11.0.3", + "@vitejs/plugin-vue": "^4.3.4", + "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.4.0", - "eslint": "^8.48.0", + "eslint": "^8.49.0", "eslint-plugin-vue": "^9.17.0", "npm-run-all": "^4.1.5", - "sass": "^1.64.2", - "terser": "^5.19.2", + "sass": "^1.66.1", + "terser": "^5.19.4", "typescript": "^5.2.2", "vite": "^4.4.9", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.3.0", - "vue-tsc": "^1.8.8" + "vue-tsc": "^1.8.11" } } diff --git a/webapp/src/components/GridProfile.vue b/webapp/src/components/GridProfile.vue new file mode 100644 index 000000000..31a141332 --- /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 458fd55c2..e64ac6d63 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -134,7 +134,9 @@ "Failure": "Fehlgeschlagen", "Pending": "Ausstehend", "Ok": "Ok", - "Unknown": "Unbekannt" + "Unknown": "Unbekannt", + "ShowGridProfile": "Zeige Grid Profil", + "GridProfile": "Grid Profil" }, "vedirecthome": { "SerialNumber": "Seriennummer: ", @@ -192,6 +194,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", @@ -308,6 +316,7 @@ "BaseTopic": "Basis Topic", "PublishInterval": "Veröffentlichungsintervall", "Seconds": "{sec} Sekunden", + "CleanSession": "CleanSession Flag", "Retain": "Retain", "Tls": "TLS", "RootCertifcateInfo": "Root CA-Zertifikat-Informationen", @@ -474,6 +483,7 @@ "BaseTopicHint": "Basis-Topic, wird allen veröffentlichten Themen vorangestellt (z.B. inverter/)", "PublishInterval": "Veröffentlichungsintervall:", "Seconds": "Sekunden", + "CleanSession": "CleanSession Flag aktivieren", "EnableRetain": "Retain Flag aktivieren", "EnableTls": "TLS aktivieren", "RootCa": "CA-Root-Zertifikat (Standard Letsencrypt):", @@ -614,6 +624,9 @@ "SaveOrder": "Reihenfolge speichern", "DeleteInverter": "Wechselrichter löschen", "EditInverter": "Wechselrichter bearbeiten", + "General": "Allgemein", + "String": "String", + "Advanced": "Erweitert", "InverterSerial": "Wechselrichter Seriennummer:", "InverterName": "Wechselrichter Name:", "InverterNameHint": "Hier kann ein eigener Namen für den Wechselrichter angeben werden.", @@ -629,6 +642,12 @@ "StringYtOffset": "Ertragsversatz String {num}:", "StringYtOffsetHint": "Dieser Offset wird beim Auslesen des Gesamtertragswertes des Wechselrichters angewendet. Damit kann der Gesamtertrag des Wechselrichters auf Null gesetzt werden, wenn ein gebrauchter Wechselrichter verwendet wird.", "InverterHint": "*) Geben Sie die Wp des Ports ein, um die Einstrahlung zu errechnen.", + "ReachableThreshold": "Erreichbarkeit Schwellenwert:", + "ReachableThresholdHint": "Legt fest, wie viele Anfragen fehlschlagen dürfen, bis der Wechselrichter als unerreichbar eingestuft wird.", + "ZeroRuntime": "Nulle Laufzeit Daten", + "ZeroRuntimeHint": "Nulle Laufzeit Daten (keine Ertragsdaten), wenn der Wechselrichter nicht erreichbar ist.", + "ZeroDay": "Nulle Tagesertrag um Mitternacht", + "ZeroDayHint": "Das funktioniert nur wenn der Wechselrichter nicht erreichbar ist. Wenn Daten aus dem Wechselrichter gelesen werden, werden deren Werte verwendet. (Ein Reset erfolgt nur beim Neustarten)", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Soll der Wechselrichter \"{name}\" mit der Seriennummer {serial} wirklich gelöscht werden?", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 72296e43a..3b9d8ded0 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -134,7 +134,9 @@ "Failure": "Failure", "Pending": "Pending", "Ok": "Ok", - "Unknown": "Unknown" + "Unknown": "Unknown", + "ShowGridProfile": "Show Grid Profile", + "GridProfile": "Grid Profile" }, "vedirecthome": { "SerialNumber": "Serial Number: ", @@ -192,6 +194,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", @@ -310,6 +318,7 @@ "BaseTopic": "Base Topic", "PublishInterval": "Publish Interval", "Seconds": "{sec} seconds", + "CleanSession": "CleanSession flag", "Retain": "Retain", "Tls": "TLS", "RootCertifcateInfo": "Root CA Certifcate Info", @@ -476,6 +485,7 @@ "BaseTopicHint": "Base topic, will be prepend to all published topics (e.g. inverter/)", "PublishInterval": "Publish Interval:", "Seconds": "seconds", + "CleanSession": "Enable CleanSession flag", "EnableRetain": "Enable Retain Flag", "EnableTls": "Enable TLS", "RootCa": "CA-Root-Certificate (default Letsencrypt):", @@ -623,6 +633,9 @@ "SaveOrder": "Save order", "DeleteInverter": "Delete inverter", "EditInverter": "Edit inverter", + "General": "General", + "String": "String", + "Advanced": "Advanced", "InverterSerial": "Inverter Serial:", "InverterName": "Inverter Name:", "InverterNameHint": "Here you can specify a custom name for your inverter.", @@ -638,6 +651,12 @@ "StringYtOffset": "Yield total offset string {num}:", "StringYtOffsetHint": "This offset is applied the read yield total value from the inverter. This can be used to set the yield total of the inverter to zero if a used inverter is used. But you can still try polling data.", "InverterHint": "*) Enter the Wp of the channel to calculate irradiation.", + "ReachableThreshold": "Reachable Threshold:", + "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", + "ZeroRuntime": "Zero runtime data", + "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", + "ZeroDay": "Zero daily yield at midnight", + "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Are you sure you want to delete the inverter \"{name}\" with serial number {serial}?", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index a4088ea87..fd4b069ae 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -134,7 +134,9 @@ "Failure": "Échec", "Pending": "En attente", "Ok": "OK", - "Unknown": "Inconnu" + "Unknown": "Inconnu", + "ShowGridProfile": "Show Grid Profile", + "GridProfile": "Grid Profile" }, "vedirecthome": { "SerialNumber": "Serial Number: ", @@ -192,6 +194,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", @@ -308,6 +316,7 @@ "BaseTopic": "Sujet de base", "PublishInterval": "Intervalle de publication", "Seconds": "{sec} secondes", + "CleanSession": "CleanSession Flag", "Retain": "Conserver", "Tls": "TLS", "RootCertifcateInfo": "Informations sur le certificat de l'autorité de certification racine", @@ -474,6 +483,7 @@ "BaseTopicHint": "Sujet de base, qui sera ajouté en préambule à tous les sujets publiés (par exemple, inverter/).", "PublishInterval": "Intervalle de publication", "Seconds": "secondes", + "CleanSession": "Enable CleanSession flag", "EnableRetain": "Activation du maintien", "EnableTls": "Activer le TLS", "RootCa": "Certificat CA-Root (par défaut Letsencrypt)", @@ -521,6 +531,9 @@ "SaveOrder": "Save order", "DeleteInverter": "Supprimer l'onduleur", "EditInverter": "Modifier l'onduleur", + "General": "Général", + "String": "Ligne", + "Advanced": "Advanced", "InverterSerial": "Numéro de série de l'onduleur", "InverterName": "Nom de l'onduleur :", "InverterNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour votre onduleur.", @@ -536,6 +549,12 @@ "StringYtOffset": "Décalage du rendement total de la ligne {num} :", "StringYtOffsetHint": "Ce décalage est appliqué à la valeur de rendement total lue sur le variateur. Il peut être utilisé pour mettre le rendement total du variateur à zéro si un variateur usagé est utilisé.", "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", + "ReachableThreshold": "Reachable Threshold:", + "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", + "ZeroRuntime": "Zero runtime data", + "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", + "ZeroDay": "Zero daily yield at midnight", + "ZeroDayHint": "This only works if the inverter is unreachable. If data is read from the inverter, it's values will be used. (Reset only occours on power cycle)", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", "DeleteMsg": "Êtes-vous sûr de vouloir supprimer l'onduleur \"{name}\" avec le numéro de série \"{serial}\" ?", diff --git a/webapp/src/types/GridProfileStatus.ts b/webapp/src/types/GridProfileStatus.ts new file mode 100644 index 000000000..5cae865c3 --- /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/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index 0e5458f13..becd2253f 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -2,6 +2,7 @@ export interface ValueObject { v: number; // value u: string; // unit d: number; // digits + max: number; } export interface InverterStatistics { diff --git a/webapp/src/types/MqttConfig.ts b/webapp/src/types/MqttConfig.ts index c7092dcfb..787a2a7a9 100644 --- a/webapp/src/types/MqttConfig.ts +++ b/webapp/src/types/MqttConfig.ts @@ -7,6 +7,7 @@ export interface MqttConfig { mqtt_password: string; mqtt_topic: string; mqtt_publish_interval: number; + mqtt_clean_session: boolean; mqtt_retain: boolean; mqtt_tls: boolean; mqtt_root_ca_cert: string; diff --git a/webapp/src/types/MqttStatus.ts b/webapp/src/types/MqttStatus.ts index b9870a1de..0bfacb31e 100644 --- a/webapp/src/types/MqttStatus.ts +++ b/webapp/src/types/MqttStatus.ts @@ -6,6 +6,7 @@ export interface MqttStatus { mqtt_username: string; mqtt_topic: string; mqtt_publish_interval: number; + mqtt_clean_session: boolean; mqtt_retain: boolean; mqtt_tls: boolean; mqtt_root_ca_cert_info: string; diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index ab51e705b..e9fd941a0 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -78,6 +78,14 @@ +
+ +
+