diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 285678868..06d05f878 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -ko_fi: tbnobody \ No newline at end of file +ko_fi: schlimmchen diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 86c9b35ea..78b360709 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,13 +10,13 @@ body: - type: markdown attributes: value: | - #### Have a question? 👉 [Start a new discussion](https://github.com/helgeerbe/OpenDTU-OnBattery/discussions/new/choose) or [ask in chat](https://discord.gg/WzhxEY62mB). + #### Have a question? 👉 [Start a new discussion](https://github.com/hoylabs/OpenDTU-OnBattery/discussions/new/choose) or [ask in chat](https://discord.gg/WzhxEY62mB). #### Before opening an issue, please double check: - [Documentation](https://opendtu-onbattery.net) - [The FAQs](https://opendtu-onbattery.net/firmware/faq/) - - [Existing issues and discussions](https://github.com/helgeerbe/OpenDTU-OnBattery/search?q=&type=issues) + - [Existing issues and discussions](https://github.com/hoylabs/OpenDTU-OnBattery/search?q=&type=issues) - type: textarea id: what-happened attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 922b7378e..431a935b4 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://discord.gg/WzhxEY62mB about: Discuss with us on Discord - name: 🤔 Have questions or need support? - url: https://github.com/helgeerbe/OpenDTU-OnBattery/discussions - about: Use the GitHub Discussions feature \ No newline at end of file + url: https://github.com/hoylabs/OpenDTU-OnBattery/discussions + about: Use the GitHub Discussions feature diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fdd60467e..4648d9dcc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,25 +141,22 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get tags - run: git fetch --force --tags origin + - name: Get openDTU core tags + run: git fetch --force --tags https://github.com/tbnobody/OpenDTU.git - name: Get openDTU core release run: | echo "OPEN_DTU_CORE_RELEASE=$(git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | grep 'refs/tags/v' | tail -1 | sed 's#.*/##' | sed 's/ .*//')" >> $GITHUB_ENV -# disabled as uploading the changed gist failed repeatedly. -# maybe the token in secrets.GIST_SECRET has expired? -# need help from repo owner @helgeerbe to fix this. -# - name: Create openDTU-core-release-Badge -# uses: schneegans/dynamic-badges-action@v1.6.0 -# with: -# auth: ${{ secrets.GIST_SECRET }} -# gistID: 68b47cc8c8994d04ab3a4fa9d8aee5e6 -# filename: openDTUcoreRelease.json -# label: based on original OpenDTU -# message: ${{ env.OPEN_DTU_CORE_RELEASE }} -# color: lightblue + - name: Create openDTU-core-release-Badge + uses: schneegans/dynamic-badges-action@e9a478b16159b4d31420099ba146cdc50f134483 # version 1.7.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: 856dda48c1cadac6ea495213340c612b + filename: openDTUcoreRelease.json + label: based on upstream OpenDTU + message: ${{ env.OPEN_DTU_CORE_RELEASE }} + color: lightblue - name: Build Changelog id: github_release diff --git a/.github/workflows/config/release-notes-config.json b/.github/workflows/config/release-notes-config.json index a6dacfea3..785c1d40c 100644 --- a/.github/workflows/config/release-notes-config.json +++ b/.github/workflows/config/release-notes-config.json @@ -36,7 +36,7 @@ } ], "template": "${{CHANGELOG}}", - "pr_template": "- [${{TITLE}}](https://github.com/helgeerbe/OpenDTU-OnBattery/commit/${{MERGE_SHA}})", + "pr_template": "- [${{TITLE}}](https://github.com/hoylabs/OpenDTU-OnBattery/commit/${{MERGE_SHA}})", "empty_template": "- no changes", "label_extractor": [ { diff --git a/README.md b/README.md index 7ccf9522d..fa1f7b76b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ [![OpenDTU-OnBattery Build](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/build.yml) [![cpplint](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/cpplint.yml) [![Yarn Linting](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml/badge.svg)](https://github.com/hoylabs/OpenDTU-OnBattery/actions/workflows/yarnlint.yml) - +![GitHub tag (latest SemVer)](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/AndreasBoehm/856dda48c1cadac6ea495213340c612b/raw/openDTUcoreRelease.json) - [OpenDTU-OnBattery](#opendtu-onbattery) - [Getting Started](#getting-started) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index cfce8ea88..991a5b975 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -348,7 +348,5 @@ class MqttBatteryStats : public BatteryStats { // we do NOT publish the same data under a different topic. void mqttPublish() const final { } - void getLiveViewData(JsonVariant& root) const final; - bool supportsAlarmsAndWarnings() const final { return false; } }; diff --git a/include/Configuration.h b/include/Configuration.h index 2bd984f5d..cdfd5eb54 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -29,6 +29,7 @@ #define MQTT_MAX_TOPIC_STRLEN 256 #define MQTT_MAX_LWTVALUE_STRLEN 20 #define MQTT_MAX_CERT_STRLEN 2560 +#define MQTT_MAX_JSON_PATH_STRLEN 256 #define INV_MAX_NAME_STRLEN 31 #define INV_MAX_COUNT 10 @@ -47,8 +48,6 @@ #define POWERMETER_MQTT_MAX_VALUES 3 #define POWERMETER_HTTP_JSON_MAX_VALUES 3 -#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256 -#define BATTERY_JSON_MAX_PATH_STRLEN 128 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -88,7 +87,7 @@ using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; struct POWERMETER_MQTT_VALUE_T { char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + char JsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; @@ -111,7 +110,7 @@ using PowerMeterSerialSdmConfig = struct POWERMETER_SERIAL_SDM_CONFIG_T; struct POWERMETER_HTTP_JSON_VALUE_T { HttpRequestConfig HttpRequest; bool Enabled; - char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + char JsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; @@ -182,9 +181,9 @@ struct BATTERY_CONFIG_T { uint8_t JkBmsInterface; uint8_t JkBmsPollingInterval; char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttSocJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttVoltageJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; BatteryVoltageUnit MqttVoltageUnit; bool EnableDischargeCurrentLimit; float DischargeCurrentLimit; @@ -192,7 +191,7 @@ struct BATTERY_CONFIG_T { float DischargeCurrentLimitBelowVoltage; bool UseBatteryReportedDischargeCurrentLimit; char MqttDischargeCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttDischargeCurrentJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttDischargeCurrentJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; BatteryAmperageUnit MqttAmperageUnit; }; using BatteryConfig = struct BATTERY_CONFIG_T; @@ -216,13 +215,34 @@ struct GRID_CHARGER_CONFIG_T { }; using GridChargerConfig = struct GRID_CHARGER_CONFIG_T; -enum SolarChargerProviderType { VEDIRECT = 0 }; +enum SolarChargerProviderType { VEDIRECT = 0, MQTT = 1 }; + +struct SOLARCHARGER_MQTT_CONFIG_T { + bool CalculateOutputPower; + + enum WattageUnit { KiloWatts = 0, Watts = 1, MilliWatts = 2 }; + char PowerTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + WattageUnit PowerUnit; + + enum VoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; + char VoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char VoltageJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + VoltageUnit VoltageTopicUnit; + + enum AmperageUnit { Amps = 0, MilliAmps = 1 }; + char CurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char CurrentJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + AmperageUnit CurrentUnit; +}; +using SolarChargerMqttConfig = struct SOLARCHARGER_MQTT_CONFIG_T; struct SOLAR_CHARGER_CONFIG_T { bool Enabled; bool VerboseLogging; - SolarChargerProviderType Provider; bool PublishUpdatesOnly; + SolarChargerProviderType Provider; + SolarChargerMqttConfig Mqtt; }; using SolarChargerConfig = struct SOLAR_CHARGER_CONFIG_T; @@ -386,6 +406,7 @@ class ConfigurationClass { static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); static void serializeSolarChargerConfig(SolarChargerConfig const& source, JsonObject& target); + static void serializeSolarChargerMqttConfig(SolarChargerMqttConfig const& source, JsonObject& target); static void serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target); static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target); static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); @@ -396,6 +417,7 @@ class ConfigurationClass { static void deserializeHttpRequestConfig(JsonObject const& source_http_config, HttpRequestConfig& target); static void deserializeSolarChargerConfig(JsonObject const& source, SolarChargerConfig& target); + static void deserializeSolarChargerMqttConfig(JsonObject const& source, SolarChargerMqttConfig& target); static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target); static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h deleted file mode 100644 index 7e00df2cb..000000000 --- a/include/MqttHandleVedirect.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "VeDirectMpptController.h" -#include "Configuration.h" -#include -#include -#include - -class MqttHandleVedirectClass { -public: - void init(Scheduler& scheduler); - void forceUpdate(); -private: - void loop(); - std::map _kvFrames; - - Task _loopTask; - - // point of time in millis() when updated values will be published - uint32_t _nextPublishUpdatesOnly = 0; - - // point of time in millis() when all values will be published - uint32_t _nextPublishFull = 1; - - bool _PublishFull; - - void publish_mppt_data(const VeDirectMpptController::data_t &mpptData, - const VeDirectMpptController::data_t &frame) const; -}; - -extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/include/MqttSettings.h b/include/MqttSettings.h index add6a690b..63f9ad97c 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -21,7 +21,7 @@ class MqttSettingsClass { void unsubscribe(const String& topic); String getPrefix() const; - String getClientId(); + String getClientId() const; private: void NetworkEvent(network_event event); diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index c76b06d34..134d32d1b 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -74,10 +74,11 @@ class PowerLimiterClass { std::pair _nextInverterRestart = { false, 0 }; bool _fullSolarPassThroughEnabled = false; bool _verboseLogging = true; + bool _shutdownComplete = false; frozen::string const& getStatusText(Status status); void announceStatus(Status status); - bool shutdown(Status status); + bool isDisabled(); void reloadConfig(); std::pair getInverterDcVoltage(); float getBatteryVoltage(bool log = false); diff --git a/include/SolarCharger.h b/include/SolarCharger.h deleted file mode 100644 index f21011024..000000000 --- a/include/SolarCharger.h +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include -#include "SolarChargerProvider.h" -#include "VeDirectMpptController.h" - -class SolarChargerClass { -public: - void init(Scheduler&); - void updateSettings(); - - // TODO(andreasboehm): below methods are taken from VictronMppt to start abstracting - // solar chargers without breaking everything. - size_t controllerAmount() const; - uint32_t getDataAgeMillis() const; - uint32_t getDataAgeMillis(size_t idx) const; - - // total output of all MPPT charge controllers in Watts - int32_t getOutputPowerWatts() const; - - // total panel input power of all MPPT charge controllers in Watts - int32_t getPanelPowerWatts() const; - - // sum of total yield of all MPPT charge controllers in kWh - float getYieldTotal() const; - - // sum of today's yield of all MPPT charge controllers in kWh - float getYieldDay() const; - - // minimum of all MPPT charge controllers' output voltages in V - float getOutputVoltage() const; - - std::optional getData(size_t idx = 0) const; - - bool isDataValid() const; - -private: - void loop(); - - Task _loopTask; - mutable std::mutex _mutex; - std::unique_ptr _upProvider = nullptr; -}; - -extern SolarChargerClass SolarCharger; diff --git a/include/SolarChargerProvider.h b/include/SolarChargerProvider.h deleted file mode 100644 index 77321412b..000000000 --- a/include/SolarChargerProvider.h +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "VeDirectMpptController.h" - -class SolarChargerProvider { -public: - // returns true if the provider is ready for use, false otherwise - virtual bool init(bool verboseLogging) = 0; - virtual void deinit() = 0; - virtual void loop() = 0; - - // TODO(andreasboehm): below methods are taken from VictronMppt to start abstracting - // solar chargers without breaking everything. - virtual size_t controllerAmount() const = 0; - virtual uint32_t getDataAgeMillis() const = 0; - virtual uint32_t getDataAgeMillis(size_t idx) const = 0; - // total output of all MPPT charge controllers in Watts - virtual int32_t getOutputPowerWatts() const = 0; - - // total panel input power of all MPPT charge controllers in Watts - virtual int32_t getPanelPowerWatts() const = 0; - - // sum of total yield of all MPPT charge controllers in kWh - virtual float getYieldTotal() const = 0; - - // sum of today's yield of all MPPT charge controllers in kWh - virtual float getYieldDay() const = 0; - - // minimum of all MPPT charge controllers' output voltages in V - virtual float getOutputVoltage() const = 0; - - virtual std::optional getData(size_t idx = 0) const = 0; - - virtual bool isDataValid() const = 0; -}; diff --git a/include/VictronMppt.h b/include/VictronMppt.h deleted file mode 100644 index 11f4840c1..000000000 --- a/include/VictronMppt.h +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include - -#include "SolarChargerProvider.h" -#include "VeDirectMpptController.h" -#include "Configuration.h" - -class VictronMppt : public SolarChargerProvider { -public: - VictronMppt() = default; - ~VictronMppt() = default; - - bool init(bool verboseLogging) final; - void deinit() final; - void loop() final; - - bool isDataValid() const final; - - // returns the data age of all controllers, - // i.e, the youngest data's age is returned. - uint32_t getDataAgeMillis() const final; - uint32_t getDataAgeMillis(size_t idx) const final; - - size_t controllerAmount() const final { return _controllers.size(); } - std::optional getData(size_t idx = 0) const final; - - // total output of all MPPT charge controllers in Watts - int32_t getOutputPowerWatts() const final; - - // total panel input power of all MPPT charge controllers in Watts - int32_t getPanelPowerWatts() const final; - - // sum of total yield of all MPPT charge controllers in kWh - float getYieldTotal() const final; - - // sum of today's yield of all MPPT charge controllers in kWh - float getYieldDay() const final; - - // minimum of all MPPT charge controllers' output voltages in V - float getOutputVoltage() const final; - - // returns the state of operation from the first available controller - std::optional getStateOfOperation() const; - - // returns the requested value from the first available controller in mV - enum class MPPTVoltage : uint8_t { - ABSORPTION = 0, - FLOAT = 1, - BATTERY = 2 - }; - std::optional getVoltage(MPPTVoltage kindOf) const; - -private: - VictronMppt(VictronMppt const& other) = delete; - VictronMppt(VictronMppt&& other) = delete; - VictronMppt& operator=(VictronMppt const& other) = delete; - VictronMppt& operator=(VictronMppt&& other) = delete; - - mutable std::mutex _mutex; - using controller_t = std::unique_ptr; - std::vector _controllers; - - std::vector _serialPortOwners; - bool initController(int8_t rx, int8_t tx, bool logging, - uint8_t instance); -}; diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h index 9ab9c8b7c..cda5823d0 100644 --- a/include/WebApi_ws_Huawei.h +++ b/include/WebApi_ws_Huawei.h @@ -19,7 +19,7 @@ class WebApiWsHuaweiLiveClass { AsyncWebServer* _server; AsyncWebSocket _ws; - AuthenticationMiddleware _simpleDigestAuth; + AsyncAuthenticationMiddleware _simpleDigestAuth; std::mutex _mutex; diff --git a/include/WebApi_ws_battery.h b/include/WebApi_ws_battery.h index bc014d390..8de45a80d 100644 --- a/include/WebApi_ws_battery.h +++ b/include/WebApi_ws_battery.h @@ -19,7 +19,7 @@ class WebApiWsBatteryLiveClass { AsyncWebServer* _server; AsyncWebSocket _ws; - AuthenticationMiddleware _simpleDigestAuth; + AsyncAuthenticationMiddleware _simpleDigestAuth; uint32_t _lastUpdateCheck = 0; static constexpr uint16_t _responseSize = 1024 + 512; diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index b3194319d..3c42159b9 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -12,7 +12,7 @@ class WebApiWsConsoleClass { private: AsyncWebSocket _ws; - AuthenticationMiddleware _simpleDigestAuth; + AsyncAuthenticationMiddleware _simpleDigestAuth; Task _wsCleanupTask; void wsCleanupTaskCb(); diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 88654edd9..99ecd0857 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -28,7 +28,7 @@ class WebApiWsLiveClass { void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); AsyncWebSocket _ws; - AuthenticationMiddleware _simpleDigestAuth; + AsyncAuthenticationMiddleware _simpleDigestAuth; uint32_t _lastPublishOnBatteryFull = 0; uint32_t _lastPublishSolarCharger = 0; diff --git a/include/WebApi_ws_solarcharger_live.h b/include/WebApi_ws_solarcharger_live.h index b49800b1f..1a786aac9 100644 --- a/include/WebApi_ws_solarcharger_live.h +++ b/include/WebApi_ws_solarcharger_live.h @@ -16,18 +16,15 @@ class WebApiWsSolarChargerLiveClass { private: void generateCommonJsonResponse(JsonVariant& root, bool fullUpdate); - static void populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - bool hasUpdate(size_t idx); AsyncWebServer* _server; AsyncWebSocket _ws; - AuthenticationMiddleware _simpleDigestAuth; + AsyncAuthenticationMiddleware _simpleDigestAuth; uint32_t _lastFullPublish = 0; uint32_t _lastPublish = 0; - uint16_t responseSize() const; std::mutex _mutex; diff --git a/include/gridcharger/huawei/DataPoints.h b/include/gridcharger/huawei/DataPoints.h index bb5b7f49b..e3c13fd65 100644 --- a/include/gridcharger/huawei/DataPoints.h +++ b/include/gridcharger/huawei/DataPoints.h @@ -31,7 +31,7 @@ LABEL_TRAIT(InputPower, "W"); LABEL_TRAIT(InputFrequency, "Hz"); LABEL_TRAIT(InputCurrent, "A"); LABEL_TRAIT(OutputPower, "W"); -LABEL_TRAIT(Efficiency, "%"); +LABEL_TRAIT(Efficiency, ""); // no unit as value is in decimals, e.g., 0.88 for 88% LABEL_TRAIT(OutputVoltage, "V"); LABEL_TRAIT(OutputCurrentMax, "A"); LABEL_TRAIT(InputVoltage, "V"); diff --git a/include/gridcharger/huawei/HardwareInterface.h b/include/gridcharger/huawei/HardwareInterface.h index 1642f0e48..272113857 100644 --- a/include/gridcharger/huawei/HardwareInterface.h +++ b/include/gridcharger/huawei/HardwareInterface.h @@ -27,7 +27,7 @@ class HardwareInterface { }; void setParameter(Setting setting, float val); - std::unique_ptr getCurrentData() { return std::move(_upDataCurrent); } + std::unique_ptr getCurrentData(); static uint32_t constexpr DataRequestIntervalMillis = 2500; diff --git a/include/solarcharger/Controller.h b/include/solarcharger/Controller.h new file mode 100644 index 000000000..dc3f37e9a --- /dev/null +++ b/include/solarcharger/Controller.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include + +namespace SolarChargers { + +class Controller { +public: + void init(Scheduler&); + void updateSettings(); + + std::shared_ptr getStats() const; + +private: + void loop(); + + Task _loopTask; + mutable std::mutex _mutex; + std::unique_ptr _upProvider = nullptr; + bool _forcePublishSensors = false; +}; + +} // namespace SolarChargers + +extern SolarChargers::Controller SolarCharger; diff --git a/include/solarcharger/DummyStats.h b/include/solarcharger/DummyStats.h new file mode 100644 index 000000000..2329fabe5 --- /dev/null +++ b/include/solarcharger/DummyStats.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace SolarChargers { + +class DummyStats : public Stats { +public: + uint32_t getAgeMillis() const final { return 0; } + std::optional getOutputPowerWatts() const final { return std::nullopt; } + std::optional getOutputVoltage() const final { return std::nullopt; } + std::optional getPanelPowerWatts() const final { return std::nullopt; } + std::optional getYieldTotal() const final { return std::nullopt; } + std::optional getYieldDay() const final { return std::nullopt; } + void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final {} + void mqttPublish() const final {} + void mqttPublishSensors(const boolean forcePublish) const final {} +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/HassIntegration.h b/include/solarcharger/HassIntegration.h new file mode 100644 index 000000000..7f1a3014c --- /dev/null +++ b/include/solarcharger/HassIntegration.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace SolarChargers { + +class HassIntegration { +protected: + void publish(const String& subtopic, const String& payload) const; +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/Provider.h b/include/solarcharger/Provider.h new file mode 100644 index 000000000..8428b7d95 --- /dev/null +++ b/include/solarcharger/Provider.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +namespace SolarChargers { + +class Stats; +class HassIntegration; + +class Provider { +public: + // returns true if the provider is ready for use, false otherwise + virtual bool init(bool verboseLogging) = 0; + virtual void deinit() = 0; + virtual void loop() = 0; + virtual std::shared_ptr getStats() const = 0; +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/Stats.h b/include/solarcharger/Stats.h new file mode 100644 index 000000000..7f4b8cc51 --- /dev/null +++ b/include/solarcharger/Stats.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +namespace SolarChargers { + +class Stats { +public: + // the last time *any* data was updated + virtual uint32_t getAgeMillis() const; + + // total output of all MPPT charge controllers in Watts + virtual std::optional getOutputPowerWatts() const; + + // minimum of all MPPT charge controllers' output voltages in V + virtual std::optional getOutputVoltage() const; + + // total panel input power of all MPPT charge controllers in Watts + virtual std::optional getPanelPowerWatts() const; + + // sum of total yield of all MPPT charge controllers in kWh + virtual std::optional getYieldTotal() const; + + // sum of today's yield of all MPPT charge controllers in Wh + virtual std::optional getYieldDay() const; + + // convert stats to JSON for web application live view + virtual void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const; + + void mqttLoop(); + + virtual void mqttPublishSensors(const boolean forcePublish) const; + + // the interval at which all data will be re-published, even + // if they did not change. used to calculate Home Assistent expiration. + uint32_t getMqttFullPublishIntervalMs() const; + +protected: + virtual void mqttPublish() const; + +private: + uint32_t _lastMqttPublish = 0; +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/mqtt/Provider.h b/include/solarcharger/mqtt/Provider.h new file mode 100644 index 000000000..418479b1e --- /dev/null +++ b/include/solarcharger/mqtt/Provider.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace SolarChargers::Mqtt { + +class Provider : public ::SolarChargers::Provider { +public: + Provider() = default; + ~Provider() = default; + + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final { return; } // this class is event-driven + std::shared_ptr<::SolarChargers::Stats> getStats() const final { return _stats; } + +private: + Provider(Provider const& other) = delete; + Provider(Provider&& other) = delete; + Provider& operator=(Provider const& other) = delete; + Provider& operator=(Provider&& other) = delete; + + bool _verboseLogging = false; + String _outputPowerTopic; + String _outputVoltageTopic; + String _outputCurrentTopic; + std::vector _subscribedTopics; + std::shared_ptr _stats = std::make_shared(); + + void onMqttMessageOutputPower(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; + + void onMqttMessageOutputVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; + + void onMqttMessageOutputCurrent(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; +}; + +} // namespace SolarChargers::Mqtt diff --git a/include/solarcharger/mqtt/Stats.h b/include/solarcharger/mqtt/Stats.h new file mode 100644 index 000000000..563802ed3 --- /dev/null +++ b/include/solarcharger/mqtt/Stats.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace SolarChargers::Mqtt { + +class Stats : public ::SolarChargers::Stats { +friend class Provider; + +public: + // the last time *any* data was updated + uint32_t getAgeMillis() const final { return millis() - _lastUpdate; } + + std::optional getOutputPowerWatts() const final; + std::optional getOutputVoltage() const final; + std::optional getPanelPowerWatts() const final { return std::nullopt; } + std::optional getYieldTotal() const final { return std::nullopt; } + std::optional getYieldDay() const final { return std::nullopt; } + + void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final; + + // no need to republish values received via mqtt + void mqttPublish() const final {} + + // no need to republish values received via mqtt + void mqttPublishSensors(const boolean forcePublish) const final {} + +protected: + std::optional getOutputCurrent() const; + + void setOutputPowerWatts(const float powerWatts) { + _outputPowerWatts = powerWatts; + _lastUpdateOutputPowerWatts = _lastUpdate = millis(); + } + + void setOutputVoltage(const float voltage); + + void setOutputCurrent(const float current); + +private: + uint32_t _lastUpdate = 0; + + float _outputPowerWatts = 0; + uint32_t _lastUpdateOutputPowerWatts = 0; + + float _outputVoltage = 0; + uint32_t _lastUpdateOutputVoltage = 0; + + float _outputCurrent = 0; + uint32_t _lastUpdateOutputCurrent = 0; + + std::optional getValueIfNotOutdated(const uint32_t lastUpdate, const float value) const; +}; + +} // namespace SolarChargers::Mqtt diff --git a/include/MqttHandleVedirectHass.h b/include/solarcharger/victron/HassIntegration.h similarity index 64% rename from include/MqttHandleVedirectHass.h rename to include/solarcharger/victron/HassIntegration.h index b8ea4534b..33e6ae61e 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/solarcharger/victron/HassIntegration.h @@ -2,32 +2,27 @@ #pragma once #include +#include #include "VeDirectMpptController.h" -#include -class MqttHandleVedirectHassClass { +namespace SolarChargers::Victron { + +class HassIntegration : public ::SolarChargers::HassIntegration { public: - void init(Scheduler& scheduler); - void forceUpdate(); + void publishSensors(const VeDirectMpptController::data_t &mpptData) const; private: - void loop(); - void publishConfig(); - void publish(const String& subtopic, const String& payload); void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, const char *payload_on, const char *payload_off, - const VeDirectMpptController::data_t &mpptData); + const VeDirectMpptController::data_t &mpptData) const; + void publishSensor(const char *caption, const char *icon, const char *subTopic, const char *deviceClass, const char *stateClass, const char *unitOfMeasurement, - const VeDirectMpptController::data_t &mpptData); - void createDeviceInfo(JsonObject &object, - const VeDirectMpptController::data_t &mpptData); + const VeDirectMpptController::data_t &mpptData) const; - Task _loopTask; - - bool _wasConnected = false; - bool _updateForced = false; + void createDeviceInfo(JsonObject &object, + const VeDirectMpptController::data_t &mpptData) const; }; -extern MqttHandleVedirectHassClass MqttHandleVedirectHass; +} // namespace SolarChargers::Victron diff --git a/include/solarcharger/victron/Provider.h b/include/solarcharger/victron/Provider.h new file mode 100644 index 000000000..6d4c507c8 --- /dev/null +++ b/include/solarcharger/victron/Provider.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace SolarChargers::Victron { + +class Provider : public ::SolarChargers::Provider { +public: + Provider() = default; + ~Provider() = default; + + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final; + std::shared_ptr<::SolarChargers::Stats> getStats() const final { return _stats; } + +private: + Provider(Provider const& other) = delete; + Provider(Provider&& other) = delete; + Provider& operator=(Provider const& other) = delete; + Provider& operator=(Provider&& other) = delete; + + mutable std::mutex _mutex; + using controller_t = std::unique_ptr; + std::vector _controllers; + std::vector _serialPortOwners; + std::shared_ptr _stats = std::make_shared(); + + bool initController(int8_t rx, int8_t tx, bool logging, uint8_t instance); +}; + +} // namespace SolarChargers::Victron diff --git a/include/solarcharger/victron/Stats.h b/include/solarcharger/victron/Stats.h new file mode 100644 index 000000000..e54ff6ba1 --- /dev/null +++ b/include/solarcharger/victron/Stats.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include + +namespace SolarChargers::Victron { + +class Stats : public ::SolarChargers::Stats { +public: + uint32_t getAgeMillis() const final; + std::optional getOutputPowerWatts() const final; + std::optional getOutputVoltage() const final; + std::optional getPanelPowerWatts() const final; + std::optional getYieldTotal() const final; + std::optional getYieldDay() const final; + + void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final; + void mqttPublish() const final; + void mqttPublishSensors(const boolean forcePublish) const final; + + void update(const String serial, const std::optional mpptData, uint32_t lastUpdate) const; + +private: + // TODO(andreasboehm): _data and _lastUpdate in two different structures is not ideal and needs to change + mutable std::map> _data; + mutable std::map _lastUpdate; + + mutable std::map _previousData; + + // point of time in millis() when updated values will be published + mutable uint32_t _nextPublishUpdatesOnly = 0; + + // point of time in millis() when all values will be published + mutable uint32_t _nextPublishFull = 1; + + mutable bool _PublishFull; + + HassIntegration _hassIntegration; + + void populateJsonWithInstanceStats(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) const; + + void publishMpptData(const VeDirectMpptController::data_t &mpptData, const VeDirectMpptController::data_t &frame) const; +}; + +} // namespace SolarChargers::Victron diff --git a/lang/es.lang.json b/lang/es.lang.json index 629b97990..d28d9b67d 100644 --- a/lang/es.lang.json +++ b/lang/es.lang.json @@ -45,7 +45,9 @@ "Refreshing": "Refrescando", "Pull": "Tira hacia abajo para refrescar", "Release": "Soltar para refrescar", - "Close": "Cerrar" + "Close": "Cerrar", + "Yes": "Yes", + "No": "No" }, "wait": { "NotReady": "OpenDTU is not yet ready", @@ -193,7 +195,10 @@ "FirmwareVersion": "Versión del firmware", "FirmwareBuildDate": "Fecha de construcción del firmware", "HardwarePartNumber": "Número de parte de hardware", - "HardwareVersion": "Versión de hardware" + "HardwareVersion": "Versión de hardware", + "SupportsPowerDistributionLogic": "'Power Distribution Logic' supported", + "Yes": "@:base.Yes", + "No": "@:base.No" }, "gridprofile": { "NoInfo": "@:devinfo.NoInfo", @@ -637,7 +642,8 @@ "TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.", "TimeSyncLink": "Por favor, verifica la configuración de tu hora.", "DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.", - "DefaultPasswordLink": "Por favor, cambia la contraseña." + "DefaultPasswordLink": "Por favor, cambia la contraseña.", + "PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (pin_mapping.json) or have not selected a profile defined there. Please refer to the documentation for details." }, "deviceadmin": { "DeviceManager": "Administrador de Dispositivos", diff --git a/lang/it.lang.json b/lang/it.lang.json index eaafda630..5a670665d 100644 --- a/lang/it.lang.json +++ b/lang/it.lang.json @@ -45,7 +45,9 @@ "Refreshing": "Aggiorna", "Pull": "Trascina in basso per aggiornare", "Release": "Rilascia per aggiornare", - "Close": "Chiudi" + "Close": "Chiudi", + "Yes": "Yes", + "No": "No" }, "wait": { "NotReady": "OpenDTU is not yet ready", @@ -193,7 +195,10 @@ "FirmwareVersion": "Versione Firmware", "FirmwareBuildDate": "Data Firmware", "HardwarePartNumber": "Hardware Part Number", - "HardwareVersion": "Hardware Version" + "HardwareVersion": "Hardware Version", + "SupportsPowerDistributionLogic": "'Power Distribution Logic' supported", + "Yes": "@:base.Yes", + "No": "@:base.No" }, "gridprofile": { "NoInfo": "@:devinfo.NoInfo", @@ -637,7 +642,8 @@ "TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.", "TimeSyncLink": "Controlla le impostazioni Data/Ora.", "DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.", - "DefaultPasswordLink": "Per favore cambia la password." + "DefaultPasswordLink": "Per favore cambia la password.", + "PinMappingIssue": "You are using a generic firmware image, but have not yet uploaded a file with device profiles (pin_mapping.json) or have not selected a profile defined there. Please refer to the documentation for details." }, "deviceadmin": { "DeviceManager": "Device-Manager", diff --git a/lib/CpuTemperature/src/CpuTemperature.cpp b/lib/CpuTemperature/src/CpuTemperature.cpp index 60e3fc7b4..0ce68d5ee 100644 --- a/lib/CpuTemperature/src/CpuTemperature.cpp +++ b/lib/CpuTemperature/src/CpuTemperature.cpp @@ -19,6 +19,12 @@ CpuTemperatureClass CpuTemperature; float CpuTemperatureClass::read() { +#ifdef CONFIG_IDF_TARGET_ESP32S2 + // Disabling temperature reading for ESP32-S2 models as it might lead to WDT resets. + // See: https://github.com/espressif/esp-idf/issues/8088 + return NAN; +#endif + std::lock_guard lock(_mutex); float temperature = NAN; diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 662ab6d10..7fdcd09dd 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -57,7 +57,7 @@ void HoymilesClass::loop() } } - if (iv != nullptr && iv->getRadio()->isInitialized() && iv->getRadio()->isQueueEmpty()) { + if (iv != nullptr && iv->getRadio()->isInitialized()) { if (iv->getZeroValuesIfUnreachable() && !iv->isReachable()) { iv->Statistics()->zeroRuntimeData(); @@ -119,6 +119,7 @@ void HoymilesClass::loop() iv->sendGridOnProFileParaRequest(); } + _messageOutput->printf("Queue size - NRF: %" PRId32 " CMT: %" PRId32 "\r\n", _radioNrf->getQueueSize(), _radioCmt->getQueueSize()); _lastPoll = millis(); } @@ -229,6 +230,7 @@ void HoymilesClass::removeInverterBySerial(const uint64_t serial) for (uint8_t i = 0; i < _inverters.size(); i++) { if (_inverters[i]->serial() == serial) { std::lock_guard lock(_mutex); + _inverters[i]->getRadio()->removeCommands(_inverters[i].get()); _inverters.erase(_inverters.begin() + i); return; } diff --git a/lib/Hoymiles/src/HoymilesRadio.cpp b/lib/Hoymiles/src/HoymilesRadio.cpp index ea039dc90..d68b32f3c 100644 --- a/lib/Hoymiles/src/HoymilesRadio.cpp +++ b/lib/Hoymiles/src/HoymilesRadio.cpp @@ -156,6 +156,16 @@ bool HoymilesRadio::isInitialized() const return _isInitialized; } +void HoymilesRadio::removeCommands(InverterAbstract* inv) +{ + _commandQueue.removeAllEntriesForInverter(inv); +} + +uint8_t HoymilesRadio::countSimilarCommands(std::shared_ptr cmd) +{ + return _commandQueue.countSimilarCommands(cmd); +} + bool HoymilesRadio::isIdle() const { return !_busyFlag; @@ -165,3 +175,8 @@ bool HoymilesRadio::isQueueEmpty() const { return _commandQueue.size() == 0; } + +uint32_t HoymilesRadio::getQueueSize() const +{ + return _commandQueue.size(); +} diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 296b479bb..95dfe8b01 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,11 +1,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Arduino.h" #include "commands/CommandAbstract.h" +#include "queue/CommandQueue.h" #include "types.h" -#include #include -#include + +#ifdef HOY_DEBUG_QUEUE +#define DEBUG_PRINT(fmt, args...) Serial.printf(fmt, ##args) +#else +#define DEBUG_PRINT(fmt, args...) /* Don't do anything in release builds */ +#endif class HoymilesRadio { public: @@ -14,11 +20,48 @@ class HoymilesRadio { bool isIdle() const; bool isQueueEmpty() const; + uint32_t getQueueSize() const; bool isInitialized() const; + void removeCommands(InverterAbstract* inv); + uint8_t countSimilarCommands(std::shared_ptr cmd); + void enqueCommand(std::shared_ptr cmd) { + DEBUG_PRINT("Queue size before: %ld\r\n", _commandQueue.size()); + DEBUG_PRINT("Handling command %s with type %d\r\n", cmd.get()->getCommandName().c_str(), static_cast(cmd.get()->getQueueInsertType())); + switch (cmd.get()->getQueueInsertType()) { + case QueueInsertType::RemoveOldest: + _commandQueue.removeDuplicatedEntries(cmd); + break; + case QueueInsertType::ReplaceExistent: + // Checks if the queue already contains a command like the new one + // and replaces the existing one with the new one. + // (The new one will not be pushed at the end of the queue) + if (_commandQueue.countSimilarCommands(cmd) > 0) { + DEBUG_PRINT(" ... existing entry will be replaced\r\n"); + _commandQueue.replaceEntries(cmd); + return; + } + break; + case QueueInsertType::RemoveNewest: + // Checks if the queue already contains a command like the new one + // and drops the new one. The new one will not be inserted. + if (_commandQueue.countSimilarCommands(cmd) > 0) { + DEBUG_PRINT(" ... new entry will be dropped\r\n"); + return; + } + break; + case QueueInsertType::AllowMultiple: + // Dont do anything, just fall through and insert the command. + break; + } + + // Push the command into the queue if we reach this position of the code + DEBUG_PRINT(" ... new entry will be appended\r\n"); _commandQueue.push(cmd); + + DEBUG_PRINT("Queue size after: %ld\r\n", _commandQueue.size()); } template @@ -38,7 +81,7 @@ class HoymilesRadio { void handleReceivedPackage(); serial_u _dtuSerial; - ThreadSafeQueue> _commandQueue; + CommandQueue _commandQueue; bool _isInitialized = false; bool _busyFlag = false; diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 4ce3c6e55..4ceb69ab2 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -44,7 +44,15 @@ ActivePowerControlCommand::ActivePowerControlCommand(InverterAbstract* inv, cons String ActivePowerControlCommand::getCommandName() const { - return "ActivePowerControl"; + char buffer[30]; + snprintf(buffer, sizeof(buffer), "ActivePowerControl (%02X)", getType()); + return buffer; +} + +bool ActivePowerControlCommand::areSameParameter(CommandAbstract* other) +{ + return CommandAbstract::areSameParameter(other) + && this->getType() == static_cast(other)->getType(); } void ActivePowerControlCommand::setActivePowerLimit(const float limit, const PowerLimitControlType type) @@ -79,7 +87,10 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons } } _inv->SystemConfigPara()->setLastUpdateCommand(millis()); - _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); + std::shared_ptr cmd(std::shared_ptr(), this); + if (_inv->getRadio()->countSimilarCommands(cmd) == 1) { + _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); + } return true; } @@ -89,7 +100,7 @@ float ActivePowerControlCommand::getLimit() const return l / 10; } -PowerLimitControlType ActivePowerControlCommand::getType() +PowerLimitControlType ActivePowerControlCommand::getType() const { return (PowerLimitControlType)((static_cast(_payload[14]) << 8) | _payload[15]); } diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h index 375b278bb..8425d248f 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h @@ -15,11 +15,13 @@ class ActivePowerControlCommand : public DevControlCommand { explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); virtual String getCommandName() const; + virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::RemoveOldest; } + virtual bool areSameParameter(CommandAbstract* other); virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual void gotTimeout(); void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent); float getLimit() const; - PowerLimitControlType getType(); + PowerLimitControlType getType() const; }; diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index 16a7857e1..6196a96f9 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -138,3 +138,9 @@ uint8_t CommandAbstract::getMaxRetransmitCount() const { return MAX_RETRANSMIT_COUNT; } + +bool CommandAbstract::areSameParameter(CommandAbstract* other) +{ + return this->getCommandName() == other->getCommandName() + && this->_targetAddress == other->getTargetAddress(); +} diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index c93cb3416..64c0bcd4a 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -11,6 +11,18 @@ class InverterAbstract; +enum class QueueInsertType { + AllowMultiple, + // Remove from beginning of the queue + RemoveOldest, + + // Don't insert command if it already exist + RemoveNewest, + + // Replace the existing entry in the queue by the one to be added + ReplaceExistent, +}; + class CommandAbstract { public: explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0); @@ -46,6 +58,10 @@ class CommandAbstract { // Sets the amount how often a missing fragment is re-requested if it was not available virtual uint8_t getMaxRetransmitCount() const; + // Returns whether multiple instances of this command are allowed in the command queue. + virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::RemoveNewest; } + virtual bool areSameParameter(CommandAbstract* other); + protected: uint8_t _payload[RF_LEN]; uint8_t _payload_size; diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h index d40c356db..a86b56786 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.h +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -8,6 +8,7 @@ class PowerControlCommand : public DevControlCommand { explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); virtual String getCommandName() const; + virtual QueueInsertType getQueueInsertType() const { return QueueInsertType::AllowMultiple; } virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual void gotTimeout(); diff --git a/lib/Hoymiles/src/inverters/HERF_1CH.cpp b/lib/Hoymiles/src/inverters/HERF_1CH.cpp index 17e0a6183..6835bf7f9 100644 --- a/lib/Hoymiles/src/inverters/HERF_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HERF_1CH.cpp @@ -34,7 +34,9 @@ static const channelMetaData_t channelMetaData[] = { }; HERF_1CH::HERF_1CH(HoymilesRadio* radio, const uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} bool HERF_1CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.cpp b/lib/Hoymiles/src/inverters/HERF_2CH.cpp index 14f4644bc..0fd81df74 100644 --- a/lib/Hoymiles/src/inverters/HERF_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HERF_2CH.cpp @@ -42,7 +42,9 @@ static const channelMetaData_t channelMetaData[] = { }; HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} bool HERF_2CH::isValidSerial(const uint64_t serial) { @@ -53,7 +55,7 @@ bool HERF_2CH::isValidSerial(const uint64_t serial) String HERF_2CH::typeName() const { - return "HERF-800-2T"; + return "HERF-600/800-2T"; } const byteAssign_t* HERF_2CH::getByteAssignment() const diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.cpp b/lib/Hoymiles/src/inverters/HERF_4CH.cpp index dcd01b6d5..a3484734a 100644 --- a/lib/Hoymiles/src/inverters/HERF_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HERF_4CH.cpp @@ -5,7 +5,9 @@ #include "HERF_4CH.h" HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial) - : HM_4CH(radio, serial) {}; + : HM_4CH(radio, serial) +{ +} bool HERF_4CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index a04b0b7d2..a5ae00d59 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -33,7 +33,9 @@ static const channelMetaData_t channelMetaData[] = { }; HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) - : HMS_Abstract(radio, serial) {}; + : HMS_Abstract(radio, serial) +{ +} bool HMS_1CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp index 3716cd98f..024b15f5a 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp @@ -33,18 +33,20 @@ static const channelMetaData_t channelMetaData[] = { }; HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) - : HMS_Abstract(radio, serial) {}; + : HMS_Abstract(radio, serial) +{ +} bool HMS_1CHv2::isValidSerial(const uint64_t serial) { // serial >= 0x112500000000 && serial <= 0x1125ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; - return preSerial == 0x1125; + return preSerial == 0x1125 || preSerial == 0x1400; } String HMS_1CHv2::typeName() const { - return "HMS-500-1T v2"; + return "HMS-450/500-1T v2"; } const byteAssign_t* HMS_1CHv2::getByteAssignment() const diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index c22fcf0d4..fa7d5e3bf 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -41,7 +41,9 @@ static const channelMetaData_t channelMetaData[] = { }; HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) - : HMS_Abstract(radio, serial) {}; + : HMS_Abstract(radio, serial) +{ +} bool HMS_2CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index 0d8d16702..791259d28 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -57,7 +57,9 @@ static const channelMetaData_t channelMetaData[] = { }; HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) - : HMS_Abstract(radio, serial) {}; + : HMS_Abstract(radio, serial) +{ +} bool HMS_4CH::isValidSerial(const uint64_t serial) { @@ -81,6 +83,13 @@ uint8_t HMS_4CH::getByteAssignmentSize() const return sizeof(byteAssignment) / sizeof(byteAssignment[0]); } +bool HMS_4CH::supportsPowerDistributionLogic() +{ + // This feature was added in inverter firmware version 01.01.12 and + // will limit the AC output instead of limiting the DC inputs. + return DevInfo()->getFwBuildVersion() >= 10112U; +} + const channelMetaData_t* HMS_4CH::getChannelMetaData() const { return channelMetaData; @@ -90,10 +99,3 @@ uint8_t HMS_4CH::getChannelMetaDataSize() const { return sizeof(channelMetaData) / sizeof(channelMetaData[0]); } - -bool HMS_4CH::supportsPowerDistributionLogic() -{ - // This feature was added in inverter firmware version 01.01.12 and - // will limit the AC output instead of limiting the DC inputs. - return DevInfo()->getFwBuildVersion() >= 10112U; -}; diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.h b/lib/Hoymiles/src/inverters/HMS_4CH.h index fb2d1662f..9bd0b1706 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.h +++ b/lib/Hoymiles/src/inverters/HMS_4CH.h @@ -10,7 +10,7 @@ class HMS_4CH : public HMS_Abstract { String typeName() const; const byteAssign_t* getByteAssignment() const; uint8_t getByteAssignmentSize() const; + bool supportsPowerDistributionLogic() final; const channelMetaData_t* getChannelMetaData() const; uint8_t getChannelMetaDataSize() const; - bool supportsPowerDistributionLogic() final; }; diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index 48caf8ce8..7e2e05f9b 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -66,7 +66,9 @@ static const channelMetaData_t channelMetaData[] = { }; HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) - : HMT_Abstract(radio, serial) {}; + : HMT_Abstract(radio, serial) +{ +} bool HMT_4CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 9e835671d..6adf2112b 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -82,7 +82,9 @@ static const channelMetaData_t channelMetaData[] = { }; HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) - : HMT_Abstract(radio, serial) {}; + : HMT_Abstract(radio, serial) +{ +} bool HMT_6CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp index 50c895cc6..095b3d2e9 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -12,7 +12,7 @@ HMT_Abstract::HMT_Abstract(HoymilesRadio* radio, const uint64_t serial) : HM_Abstract(radio, serial) { EventLog()->setMessageType(AlarmMessageType_t::HMT); -}; +} bool HMT_Abstract::sendChangeChannelRequest() { @@ -26,4 +26,4 @@ bool HMT_Abstract::sendChangeChannelRequest() _radio->enqueCommand(cmdChannel); return true; -}; +} diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 37761942b..cd241228f 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -33,7 +33,9 @@ static const channelMetaData_t channelMetaData[] = { }; HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} bool HM_1CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 28778463d..4ea19a8a5 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -42,7 +42,9 @@ static const channelMetaData_t channelMetaData[] = { }; HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} bool HM_2CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index 4d90e4ad4..11534e214 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -57,7 +57,9 @@ static const channelMetaData_t channelMetaData[] = { }; HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) - : HM_Abstract(radio, serial) {}; + : HM_Abstract(radio, serial) +{ +} bool HM_4CH::isValidSerial(const uint64_t serial) { diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 9f426af4b..fc818714f 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -14,7 +14,9 @@ #include "commands/SystemConfigParaCommand.h" HM_Abstract::HM_Abstract(HoymilesRadio* radio, const uint64_t serial) - : InverterAbstract(radio, serial) {}; + : InverterAbstract(radio, serial) +{ +} bool HM_Abstract::sendStatsRequest() { @@ -225,3 +227,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest() return true; } + +bool HM_Abstract::supportsPowerDistributionLogic() +{ + return false; +} diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.h b/lib/Hoymiles/src/inverters/HM_Abstract.h index 1088403ba..9f5c4b31a 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.h +++ b/lib/Hoymiles/src/inverters/HM_Abstract.h @@ -16,7 +16,7 @@ class HM_Abstract : public InverterAbstract { bool sendRestartControlRequest(); bool resendPowerControlRequest(); bool sendGridOnProFileParaRequest(); - bool supportsPowerDistributionLogic() override { return false; }; + bool supportsPowerDistributionLogic() override; private: uint8_t _lastAlarmLogCnt = 0; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index b55445328..93878b139 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -1,16 +1,16 @@ # Class overview | Class | Models | Serial range | -| --------------| --------------------------- | ------------- -- | +| ------------- | --------------------------- | ---------------- | | HM_1CH | HM-300/350/400-1T | 1121 | | HM_2CH | HM-600/700/800-2T | 1141 | | HM_4CH | HM-1000/1200/1500-4T | 1161 | | HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | -| HMS_1CHv2 | HMS-500-1T v2 | 1125 | +| HMS_1CHv2 | HMS-450/500-1T v2 | 1125, 1400 | | HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144, 1410 | | HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_6CH | HMT-1800/2250-6T | 1382 | | HERF_1CH | HERF 300 | 2841 | -| HERF_2CH | HERF 800 | 2821 | +| HERF_2CH | HERF 600/800 | 2821 | | HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index fb0fe3e89..b0bd11dd8 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -81,6 +81,7 @@ const devInfo_t devInfo[] = { { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 + { { 0xF1, 0x01, 0x10, ALL }, 600, "HERF-600" }, // 00 { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 diff --git a/lib/Hoymiles/src/queue/CommandQueue.cpp b/lib/Hoymiles/src/queue/CommandQueue.cpp new file mode 100644 index 000000000..e4b6069ab --- /dev/null +++ b/lib/Hoymiles/src/queue/CommandQueue.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "CommandQueue.h" +#include "../inverters/InverterAbstract.h" +#include + +void CommandQueue::removeAllEntriesForInverter(InverterAbstract* inv) +{ + std::lock_guard lock(_mutex); + + auto it = std::remove_if(_queue.begin(), _queue.end(), + [&inv](std::shared_ptr v) -> bool { return v.get()->getTargetAddress() == inv->serial(); }); + _queue.erase(it, _queue.end()); +} + +void CommandQueue::removeDuplicatedEntries(std::shared_ptr cmd) +{ + std::lock_guard lock(_mutex); + + auto it = std::remove_if(_queue.begin() + 1, _queue.end(), + [&cmd](std::shared_ptr v) -> bool { + return cmd->areSameParameter(v.get()) + && cmd.get()->getQueueInsertType() == QueueInsertType::RemoveOldest; + }); + _queue.erase(it, _queue.end()); +} + +void CommandQueue::replaceEntries(std::shared_ptr cmd) +{ + std::lock_guard lock(_mutex); + + std::replace_if(_queue.begin() + 1, _queue.end(), + [&cmd](std::shared_ptr v)-> bool { + return cmd.get()->getQueueInsertType() == QueueInsertType::ReplaceExistent + && cmd->areSameParameter(v.get()); + }, + cmd + ); +} + +uint8_t CommandQueue::countSimilarCommands(std::shared_ptr cmd) +{ + std::lock_guard lock(_mutex); + + return std::count_if(_queue.begin(), _queue.end(), + [&cmd](std::shared_ptr v) -> bool { + return cmd->areSameParameter(v.get()); + }); +} diff --git a/lib/Hoymiles/src/queue/CommandQueue.h b/lib/Hoymiles/src/queue/CommandQueue.h new file mode 100644 index 000000000..f27aeaa3b --- /dev/null +++ b/lib/Hoymiles/src/queue/CommandQueue.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "../commands/CommandAbstract.h" +#include +#include + +class InverterAbstract; + +class CommandQueue : public ThreadSafeQueue> { +public: + void removeAllEntriesForInverter(InverterAbstract* inv); + void removeDuplicatedEntries(std::shared_ptr cmd); + void replaceEntries(std::shared_ptr cmd); + + uint8_t countSimilarCommands(std::shared_ptr cmd); +}; diff --git a/lib/ThreadSafeQueue/src/ThreadSafeQueue.h b/lib/ThreadSafeQueue/src/ThreadSafeQueue.h index 9a195c603..2569e630d 100644 --- a/lib/ThreadSafeQueue/src/ThreadSafeQueue.h +++ b/lib/ThreadSafeQueue/src/ThreadSafeQueue.h @@ -3,7 +3,7 @@ #include #include -#include +#include template class ThreadSafeQueue { @@ -33,14 +33,14 @@ class ThreadSafeQueue { return {}; } T tmp = _queue.front(); - _queue.pop(); + _queue.pop_front(); return tmp; } void push(const T& item) { std::lock_guard lock(_mutex); - _queue.push(item); + _queue.push_back(item); } T front() @@ -49,6 +49,10 @@ class ThreadSafeQueue { return _queue.front(); } +protected: + std::deque _queue; + mutable std::mutex _mutex; + private: // Moved out of public interface to prevent races between this // and pop(). @@ -56,7 +60,4 @@ class ThreadSafeQueue { { return _queue.empty(); } - - std::queue _queue; - mutable std::mutex _mutex; }; diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index d929a471f..af6d2cf07 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -276,7 +276,7 @@ frozen::string const& veMpptStruct::getErrAsString() const { 39, "Input shutdown (due to current flow during off mode)" }, { 40, "Input" }, { 65, "Lost communication with one of devices" }, - { 67, "Synchronisedcharging device configuration issue" }, + { 67, "Synchronised charging device configuration issue" }, { 68, "BMS connection lost" }, { 116, "Factory calibration data lost" }, { 117, "Invalid/incompatible firmware" }, diff --git a/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index c1fa9771d..2d2f3be71 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -49,7 +49,7 @@ def get_firmware_specifier_build_flag(): build_version = get_build_version() build_flag = "-D AUTO_GIT_HASH=\\\"" + build_version + "\\\"" build_branch = get_build_branch() - build_flag += " -D AUTO_GIT_BRANCH=\\\"" + branch_name + "\\\"" + build_flag += " -D AUTO_GIT_BRANCH=\\\"" + build_branch + "\\\"" return (build_flag) diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py index ec8274e21..e9647d986 100644 --- a/pio-scripts/create_factory_bin.py +++ b/pio-scripts/create_factory_bin.py @@ -130,4 +130,5 @@ def esp32_create_combined_bin(source, target, env): esptool.main(cmd) -env.AddPostAction("buildprog", esp32_create_combined_bin) +from SCons.Script import AlwaysBuild +AlwaysBuild(env.AddPostAction("buildprog", esp32_create_combined_bin)) diff --git a/platformio.ini b/platformio.ini index 42d6abf41..d2ff18714 100644 --- a/platformio.ini +++ b/platformio.ini @@ -30,6 +30,7 @@ build_flags = -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 -DEMC_TASK_STACK_SIZE=6400 +; -DHOY_DEBUG_QUEUE -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference ; Have to remove -Werror because of ; https://github.com/espressif/arduino-esp32/issues/9044 and @@ -41,10 +42,10 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESPAsyncWebServer @ 3.3.22 - bblanchon/ArduinoJson @ 7.2.0 + mathieucarbou/ESPAsyncWebServer @ 3.6.0 + bblanchon/ArduinoJson @ 7.3.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0 - nrf24/RF24 @ 1.4.10 + nrf24/RF24 @ 1.4.11 olikraus/U8g2 @ 2.36.2 buelowp/sunset @ 1.1.7 arkhipenko/TaskScheduler @ 3.8.5 @@ -81,6 +82,7 @@ upload_protocol = esptool [env:generic_esp32_4mb_no_ota] board = esp32dev build_flags = ${env.build_flags} + -DPIN_MAPPING_REQUIRED=1 board_build.partitions = partitions_custom_4mb.csv @@ -88,6 +90,7 @@ board_build.partitions = partitions_custom_4mb.csv board = esp32dev board_upload.flash_size = 8MB build_flags = ${env.build_flags} + -DPIN_MAPPING_REQUIRED=1 [env:generic_esp32_16mb_psram] @@ -96,6 +99,7 @@ board_build.flash_mode = qio board_build.partitions = partitions_custom_16mb.csv board_upload.flash_size = 16MB build_flags = ${env.build_flags} + -DPIN_MAPPING_REQUIRED=1 -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue @@ -105,6 +109,7 @@ board = esp32-c3-devkitc-02 board_build.partitions = partitions_custom_4mb.csv custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} + -DPIN_MAPPING_REQUIRED=1 [env:generic_esp32c3_usb] @@ -114,11 +119,13 @@ custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 + -DPIN_MAPPING_REQUIRED=1 [env:generic_esp32s3] board = esp32-s3-devkitc-1 build_flags = ${env.build_flags} + -DPIN_MAPPING_REQUIRED=1 [env:generic_esp32s3_usb] @@ -127,6 +134,7 @@ upload_protocol = esp-builtin build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 + -DPIN_MAPPING_REQUIRED=1 [env:olimex_esp32_poe] diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index f4e383b29..8201b31fe 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -109,15 +109,6 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const root["showIssues"] = supportsAlarmsAndWarnings(); } -void MqttBatteryStats::getLiveViewData(JsonVariant& root) const -{ - // as we don't want to repeat the data that is already shown in the live data card - // we only add the live view data here when the discharge current limit can be shown - if (isDischargeCurrentLimitValid()) { - BatteryStats::getLiveViewData(root); - } -} - void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index be45a29de..3c9dfb27b 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -59,6 +59,20 @@ void ConfigurationClass::serializeSolarChargerConfig(SolarChargerConfig const& s target["publish_updates_only"] = source.PublishUpdatesOnly; } +void ConfigurationClass::serializeSolarChargerMqttConfig(SolarChargerMqttConfig const& source, JsonObject& target) +{ + target["calculate_output_power"] = source.CalculateOutputPower; + target["power_topic"] = source.PowerTopic; + target["power_path"] = source.PowerJsonPath; + target["power_unit"] = source.PowerUnit; + target["voltage_topic"] = source.VoltageTopic; + target["voltage_path"] = source.VoltageJsonPath; + target["voltage_unit"] = source.VoltageTopicUnit; + target["current_topic"] = source.CurrentTopic; + target["current_path"] = source.CurrentJsonPath; + target["current_unit"] = source.CurrentUnit; +} + void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target) { JsonArray values = target["values"].to(); @@ -327,6 +341,9 @@ bool ConfigurationClass::write() JsonObject solarcharger = doc["solarcharger"].to(); serializeSolarChargerConfig(config.SolarCharger, solarcharger); + JsonObject solarcharger_mqtt = solarcharger["mqtt"].to(); + serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, solarcharger_mqtt); + JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; @@ -386,6 +403,20 @@ void ConfigurationClass::deserializeSolarChargerConfig(JsonObject const& source, target.PublishUpdatesOnly = source["publish_updates_only"] | SOLAR_CHARGER_PUBLISH_UPDATES_ONLY; } +void ConfigurationClass::deserializeSolarChargerMqttConfig(JsonObject const& source, SolarChargerMqttConfig& target) +{ + target.CalculateOutputPower = source["calculate_output_power"]; + strlcpy(target.PowerTopic, source["power_topic"] | "", sizeof(target.PowerTopic)); + strlcpy(target.PowerJsonPath, source["power_path"] | "", sizeof(target.PowerJsonPath)); + target.PowerUnit = source["power_unit"] | SolarChargerMqttConfig::WattageUnit::Watts; + strlcpy(target.VoltageTopic, source["voltage_topic"] | "", sizeof(target.VoltageTopic)); + strlcpy(target.VoltageJsonPath, source["voltage_path"] | "", sizeof(target.VoltageJsonPath)); + target.VoltageTopicUnit = source["voltage_unit"] | SolarChargerMqttConfig::VoltageUnit::Volts; + strlcpy(target.CurrentTopic, source["current_topic"] | "", sizeof(target.CurrentTopic)); + strlcpy(target.CurrentJsonPath, source["current_path"] | "", sizeof(target.CurrentJsonPath)); + target.CurrentUnit = source["current_unit"] | SolarChargerMqttConfig::AmperageUnit::Amps; +} + void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) { for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { @@ -694,7 +725,9 @@ bool ConfigurationClass::read() } } - deserializeSolarChargerConfig(doc["solarcharger"], config.SolarCharger); + JsonObject solarcharger = doc["solarcharger"]; + deserializeSolarChargerConfig(solarcharger, config.SolarCharger); + deserializeSolarChargerMqttConfig(solarcharger["mqtt"], config.SolarCharger.Mqtt); JsonObject powermeter = doc["powermeter"]; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; diff --git a/src/MqttHandleDtu.cpp b/src/MqttHandleDtu.cpp index df025f12c..63aec7e18 100644 --- a/src/MqttHandleDtu.cpp +++ b/src/MqttHandleDtu.cpp @@ -32,7 +32,7 @@ void MqttHandleDtuClass::loop() return; } - MqttSettings.publish("dtu/uptime", String(millis() / 1000)); + MqttSettings.publish("dtu/uptime", String(esp_timer_get_time() / 1000000)); MqttSettings.publish("dtu/ip", NetworkSettings.localIP().toString()); MqttSettings.publish("dtu/hostname", NetworkSettings.getHostname()); MqttSettings.publish("dtu/heap/size", String(ESP.getHeapSize())); diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index b7901545f..83b2e559e 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -70,6 +70,7 @@ void MqttHandleHassClass::publishConfig() publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE); publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE); + publishDtuSensor("DC Power", "dc/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE); publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC); diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp deleted file mode 100644 index ce2cf76a4..000000000 --- a/src/MqttHandleVedirect.cpp +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Helge Erbe and others - */ -#include "MqttHandleVedirect.h" -#include "MqttSettings.h" -#include "MessageOutput.h" -#include "SolarCharger.h" - -MqttHandleVedirectClass MqttHandleVedirect; - -// #define MQTTHANDLEVEDIRECT_DEBUG - -void MqttHandleVedirectClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback([this] { loop(); }); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - // initially force a full publish - this->forceUpdate(); -} - -void MqttHandleVedirectClass::forceUpdate() -{ - // initially force a full publish - _nextPublishUpdatesOnly = 0; - _nextPublishFull = 1; -} - - -void MqttHandleVedirectClass::loop() -{ - auto const& config = Configuration.get(); - if (!MqttSettings.getConnected() - || !config.SolarCharger.Enabled - || config.SolarCharger.Provider != SolarChargerProviderType::VEDIRECT) { - return; - } - - if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) { - // determine if this cycle should publish full values or updates only - if (_nextPublishFull <= _nextPublishUpdatesOnly) { - _PublishFull = true; - } else { - _PublishFull = !config.SolarCharger.PublishUpdatesOnly; - } - - #ifdef MQTTHANDLEVEDIRECT_DEBUG - MessageOutput.printf("\r\n\r\nMqttHandleVedirectClass::loop millis %lu _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", millis(), _nextPublishUpdatesOnly, _nextPublishFull); - if (_PublishFull) { - MessageOutput.println("MqttHandleVedirectClass::loop publish full"); - } else { - MessageOutput.println("MqttHandleVedirectClass::loop publish updates only"); - } - #endif - - for (int idx = 0; idx < SolarCharger.controllerAmount(); ++idx) { - std::optional optMpptData = SolarCharger.getData(idx); - if (!optMpptData.has_value()) { continue; } - - auto const& kvFrame = _kvFrames[optMpptData->serialNr_SER]; - publish_mppt_data(*optMpptData, kvFrame); - if (!_PublishFull) { - _kvFrames[optMpptData->serialNr_SER] = *optMpptData; - } - } - - // now calculate next points of time to publish - _nextPublishUpdatesOnly = millis() + (config.Mqtt.PublishInterval * 1000); - - if (_PublishFull) { - // when Home Assistant MQTT-Auto-Discovery is active, - // and "enable expiration" is active, all values must be published at - // least once before the announced expiry interval is reached - if ((config.SolarCharger.PublishUpdatesOnly) && (config.Mqtt.Hass.Enabled) && (config.Mqtt.Hass.Expire)) { - _nextPublishFull = millis() + (((config.Mqtt.PublishInterval * 3) - 1) * 1000); - - #ifdef MQTTHANDLEVEDIRECT_DEBUG - uint32_t _tmpNextFullSeconds = (config.Mqtt_PublishInterval * 3) - 1; - MessageOutput.printf("MqttHandleVedirectClass::loop _tmpNextFullSeconds %u - _nextPublishFull %u \r\n", _tmpNextFullSeconds, _nextPublishFull); - #endif - - } else { - // no future publish full needed - _nextPublishFull = UINT32_MAX; - } - } - - #ifdef MQTTHANDLEVEDIRECT_DEBUG - MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull); - #endif - } -} - -void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::data_t ¤tData, - const VeDirectMpptController::data_t &previousData) const { - String value; - String topic = "victron/"; - topic.concat(currentData.serialNr_SER); - topic.concat("/"); - -#define PUBLISH(sm, t, val) \ - if (_PublishFull || currentData.sm != previousData.sm) { \ - MqttSettings.publish(topic + t, String(val)); \ - } - - PUBLISH(productID_PID, "PID", currentData.getPidAsString().data()); - PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER); - PUBLISH(firmwareVer_FW, "FWI", currentData.getFwVersionAsInteger()); - PUBLISH(firmwareVer_FW, "FWF", currentData.getFwVersionFormatted()); - PUBLISH(firmwareVer_FW, "FW", currentData.firmwareVer_FW); - PUBLISH(firmwareVer_FWE, "FWE", currentData.firmwareVer_FWE); - PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data()); - PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data()); - PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data()); - PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data()); - PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS); - PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0); - PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0); - PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W); - PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0); - PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0); - PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W); - PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent); - PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0); - PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0); - PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W); - PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0); - PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W); -#undef PUBLILSH - -#define PUBLISH_OPT(sm, t, val) \ - if (currentData.sm.first != 0 && (_PublishFull || currentData.sm.second != previousData.sm.second)) { \ - MqttSettings.publish(topic + t, String(val)); \ - } - - PUBLISH_OPT(relayState_RELAY, "RELAY", currentData.relayState_RELAY.second ? "ON" : "OFF"); - PUBLISH_OPT(loadOutputState_LOAD, "LOAD", currentData.loadOutputState_LOAD.second ? "ON" : "OFF"); - PUBLISH_OPT(loadCurrent_IL_mA, "IL", currentData.loadCurrent_IL_mA.second / 1000.0); - PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); - PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0); - PUBLISH_OPT(BatteryAbsorptionMilliVolt, "BatteryAbsorption", currentData.BatteryAbsorptionMilliVolt.second / 1000.0); - PUBLISH_OPT(BatteryFloatMilliVolt, "BatteryFloat", currentData.BatteryFloatMilliVolt.second / 1000.0); - PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); -#undef PUBLILSH_OPT -} diff --git a/src/MqttHandleVedirectHass.cpp b/src/MqttHandleVedirectHass.cpp deleted file mode 100644 index 251bf93b8..000000000 --- a/src/MqttHandleVedirectHass.cpp +++ /dev/null @@ -1,245 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "MqttHandleVedirectHass.h" -#include "Configuration.h" -#include "MqttSettings.h" -#include "MqttHandleHass.h" -#include "NetworkSettings.h" -#include "MessageOutput.h" -#include "Utils.h" -#include "__compiled_constants.h" -#include "SolarCharger.h" - -MqttHandleVedirectHassClass MqttHandleVedirectHass; - -void MqttHandleVedirectHassClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback([this] { loop(); }); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); -} - -void MqttHandleVedirectHassClass::loop() -{ - if (!Configuration.get().Mqtt.Hass.Enabled - || !Configuration.get().SolarCharger.Enabled - || Configuration.get().SolarCharger.Provider != SolarChargerProviderType::VEDIRECT) { - return; - } - - if (_updateForced) { - publishConfig(); - _updateForced = false; - } - - if (MqttSettings.getConnected() && !_wasConnected) { - // Connection established - _wasConnected = true; - publishConfig(); - } else if (!MqttSettings.getConnected() && _wasConnected) { - // Connection lost - _wasConnected = false; - } -} - -void MqttHandleVedirectHassClass::forceUpdate() -{ - _updateForced = true; -} - -void MqttHandleVedirectHassClass::publishConfig() -{ - if (!MqttSettings.getConnected()) { - return; - } - - // device info - for (int idx = 0; idx < SolarCharger.controllerAmount(); ++idx) { - auto optMpptData = SolarCharger.getData(idx); - if (!optMpptData.has_value()) { continue; } - - publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT firmware version integer", "mdi:counter", "FWI", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT firmware version formatted", "mdi:counter", "FWF", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT firmware version FW", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT firmware version FWE", "mdi:counter", "FWE", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, *optMpptData); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", *optMpptData); - - // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", *optMpptData); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A", *optMpptData); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", *optMpptData); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", *optMpptData); - - // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", *optMpptData); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", *optMpptData); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", *optMpptData); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", *optMpptData); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", *optMpptData); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", *optMpptData); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData); - - // optional info, provided only if the charge controller delivers the information - if (optMpptData->relayState_RELAY.first != 0) { - publishBinarySensor("MPPT error relay state", "mdi:electric-switch", "RELAY", "ON", "OFF", *optMpptData); - } - if (optMpptData->loadOutputState_LOAD.first != 0) { - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData); - } - if (optMpptData->loadCurrent_IL_mA.first != 0) { - publishSensor("MPPT load current", NULL, "IL", "current", "measurement", "A", *optMpptData); - } - - // optional info, provided only if TX is connected to charge controller - if (optMpptData->NetworkTotalDcInputPowerMilliWatts.first != 0) { - publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData); - } - if (optMpptData->MpptTemperatureMilliCelsius.first != 0) { - publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "°C", *optMpptData); - } - if (optMpptData->BatteryAbsorptionMilliVolt.first != 0) { - publishSensor("Battery absorption voltage", "mdi:battery-charging-90", "BatteryAbsorption", "voltage", "measurement", "V", *optMpptData); - } - if (optMpptData->BatteryFloatMilliVolt.first != 0) { - publishSensor("Battery float voltage", "mdi:battery-charging-100", "BatteryFloat", "voltage", "measurement", "V", *optMpptData); - } - if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) { - publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "°C", *optMpptData); - } - } - - yield(); -} - -void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, - const char *deviceClass, const char *stateClass, - const char *unitOfMeasurement, - const VeDirectMpptController::data_t &mpptData) -{ - String serial = mpptData.serialNr_SER; - - String sensorId = caption; - sensorId.replace(" ", "_"); - sensorId.replace(".", ""); - sensorId.replace("(", ""); - sensorId.replace(")", ""); - sensorId.toLowerCase(); - - String configTopic = "sensor/dtu_victron_" + serial - + "/" + sensorId - + "/config"; - - String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(serial); - statTopic.concat("/"); - statTopic.concat(subTopic); - - JsonDocument root; - - root["name"] = caption; - root["stat_t"] = statTopic; - root["uniq_id"] = serial + "_" + sensorId; - - if (icon != NULL) { - root["icon"] = icon; - } - - if (unitOfMeasurement != NULL) { - root["unit_of_meas"] = unitOfMeasurement; - } - - JsonObject deviceObj = root["dev"].to(); - createDeviceInfo(deviceObj, mpptData); - - if (Configuration.get().Mqtt.Hass.Expire) { - root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; - } - if (deviceClass != NULL) { - root["dev_cla"] = deviceClass; - } - if (stateClass != NULL) { - root["stat_cla"] = stateClass; - } - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - char buffer[512]; - serializeJson(root, buffer); - publish(configTopic, buffer); - -} -void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, - const char *payload_on, const char *payload_off, - const VeDirectMpptController::data_t &mpptData) -{ - String serial = mpptData.serialNr_SER; - - String sensorId = caption; - sensorId.replace(" ", "_"); - sensorId.replace(".", ""); - sensorId.replace("(", ""); - sensorId.replace(")", ""); - sensorId.toLowerCase(); - - String configTopic = "binary_sensor/dtu_victron_" + serial - + "/" + sensorId - + "/config"; - - String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(serial); - statTopic.concat("/"); - statTopic.concat(subTopic); - - JsonDocument root; - root["name"] = caption; - root["uniq_id"] = serial + "_" + sensorId; - root["stat_t"] = statTopic; - root["pl_on"] = payload_on; - root["pl_off"] = payload_off; - - if (icon != NULL) { - root["icon"] = icon; - } - - JsonObject deviceObj = root["dev"].to(); - createDeviceInfo(deviceObj, mpptData); - - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - char buffer[512]; - serializeJson(root, buffer); - publish(configTopic, buffer); -} - -void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, - const VeDirectMpptController::data_t &mpptData) -{ - String serial = mpptData.serialNr_SER; - object["name"] = "Victron(" + serial + ")"; - object["ids"] = serial; - object["cu"] = MqttHandleHass.getDtuUrl(); - object["mf"] = "OpenDTU"; - object["mdl"] = mpptData.getPidAsString(); - object["sw"] = __COMPILED_GIT_HASH__; - object["via_device"] = MqttHandleHass.getDtuUniqueId(); -} - -void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload) -{ - String topic = Configuration.get().Mqtt.Hass.Topic; - topic += subtopic; - MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); -} diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index 63455f622..d81152b0d 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -182,7 +182,7 @@ String MqttSettingsClass::getPrefix() const return Configuration.get().Mqtt.Topic; } -String MqttSettingsClass::getClientId() +String MqttSettingsClass::getClientId() const { String clientId = Configuration.get().Mqtt.ClientId; if (clientId == "") { diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 3852db9fa..81ec21dce 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -290,8 +290,7 @@ void NetworkSettingsClass::applyConfig() MessageOutput.print("new credentials... "); WiFi.begin( Configuration.get().WiFi.Ssid, - Configuration.get().WiFi.Password, - WIFI_ALL_CHANNEL_SCAN); + Configuration.get().WiFi.Password); } else { MessageOutput.print("existing credentials... "); WiFi.begin(); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index a026666b4..fe47258c2 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -10,7 +10,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include -#include +#include #include "MessageOutput.h" #include #include @@ -83,17 +83,37 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) } /** - * returns true if the inverters' state was changed or is about to change, i.e., - * if any are actually in need of a shutdown. returns false otherwise, i.e., the - * inverters are already shut down. + * NOTE: this method relies on being called regularly, i.e., as part of the + * loop(), and that it is called *after* updateInverters(). */ -bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) +bool PowerLimiterClass::isDisabled() { - announceStatus(status); + auto const& config = Configuration.get(); + + if (!config.PowerLimiter.Enabled) { + announceStatus(Status::DisabledByConfig); + } + else if (Mode::Disabled == _mode) { + announceStatus(Status::DisabledByMqtt); + } + else { + _shutdownComplete = false; + return false; + } + + // we only shut down governed inverters once when the DPL is disabled by + // configuration or by the MQTT mode setting. afterwards, we leave the + // inverter(s) alone so they can be managed through other means. + if (_shutdownComplete) { return true; } for (auto& upInv : _inverters) { upInv->standby(); } - return updateInverters(); + // we triggered the shutdown, and we won't trigger it again until the DPL + // enabled and disabled again. we rely that updateInverters() is called + // in the DPL loop(), applying the standby limit and power state. + _shutdownComplete = true; + + return true; } void PowerLimiterClass::reloadConfig() @@ -157,15 +177,7 @@ void PowerLimiterClass::loop() return announceStatus(Status::InverterCmdPending); } - if (!config.PowerLimiter.Enabled) { - shutdown(Status::DisabledByConfig); - return; - } - - if (Mode::Disabled == _mode) { - shutdown(Status::DisabledByMqtt); - return; - } + if (isDisabled()) { return; } if (_reloadConfigFlag) { reloadConfig(); @@ -245,9 +257,17 @@ void PowerLimiterClass::loop() if (isStartThresholdReached()) { return true; } + // start a nighttime discharge cycle on a partially charged battery if + // 1. the respective switch/setting is enabled + // 2. it is now after sunset, i.e., it is nighttime + // 3. we are not already in a discharge cycle + // 4. we did not start a nighttime discharge cycle on a partially + // charged battery already (the _nighttimeDischarging flag will + // only be reset at sunrise, see above) if (config.PowerLimiter.BatteryAlwaysUseAtNight && !isDayPeriod && - !_batteryDischargeEnabled) { + !_batteryDischargeEnabled && + !_nighttimeDischarging) { _nighttimeDischarging = true; return true; } @@ -263,6 +283,12 @@ void PowerLimiterClass::loop() _oLoadCorrectedVoltage = std::nullopt; if (_verboseLogging && usesBatteryPoweredInverter()) { + MessageOutput.printf("[DPL] up %lu s, %snext inverter restart at %d s (set to %d)\r\n", + millis()/1000, + (_nextInverterRestart.first?"":"NO "), + _nextInverterRestart.second/1000, + config.PowerLimiter.RestartHour); + MessageOutput.printf("[DPL] battery interface %sabled, SoC %.1f %% (%s), age %u s (%s)\r\n", (config.Battery.Enabled?"en":"dis"), Battery.getStats()->getSoC(), @@ -291,11 +317,12 @@ void PowerLimiterClass::loop() config.PowerLimiter.FullSolarPassThroughStopVoltage); } - MessageOutput.printf("[DPL] start %sreached, stop %sreached, solar-passthrough %sabled, use at night: %s\r\n", + MessageOutput.printf("[DPL] start %sreached, stop %sreached, solar-passthrough %sabled, use at night %sabled and %s\r\n", (isStartThresholdReached()?"":"NOT "), (isStopThresholdReached()?"":"NOT "), (config.PowerLimiter.SolarPassThroughEnabled?"en":"dis"), - (config.PowerLimiter.BatteryAlwaysUseAtNight?"yes":"no")); + (config.PowerLimiter.BatteryAlwaysUseAtNight?"en":"dis"), + (_nighttimeDischarging?"active":"dormant")); MessageOutput.printf("[DPL] total max AC power is %u W, conduction losses are %u %%\r\n", config.PowerLimiter.TotalUpperPowerLimit, @@ -370,8 +397,10 @@ float PowerLimiterClass::getBatteryVoltage(bool log) { if (inverter.first > 0) { res = inverter.first; } float chargeControllerVoltage = -1; - if (SolarCharger.isDataValid()) { - res = chargeControllerVoltage = static_cast(SolarCharger.getOutputVoltage()); + + auto chargerOutputVoltage = SolarCharger.getStats()->getOutputVoltage(); + if (chargerOutputVoltage) { + res = chargeControllerVoltage = *chargerOutputVoltage; } float bmsVoltage = -1; @@ -425,8 +454,9 @@ void PowerLimiterClass::fullSolarPassthrough(PowerLimiterClass::Status reason) uint16_t targetOutput = 0; - if (SolarCharger.isDataValid()) { - targetOutput = static_cast(std::max(0, SolarCharger.getOutputPowerWatts())); + auto solarChargerOuput = SolarCharger.getStats()->getOutputPowerWatts(); + if (solarChargerOuput) { + targetOutput = static_cast(std::max(0, *solarChargerOuput)); targetOutput = dcPowerBusToInverterAc(targetOutput); } @@ -675,15 +705,17 @@ bool PowerLimiterClass::updateInverters() uint16_t PowerLimiterClass::getSolarPassthroughPower() { auto const& config = Configuration.get(); + auto solarChargerOutput = SolarCharger.getStats()->getOutputPowerWatts(); if (!config.SolarCharger.Enabled || !config.PowerLimiter.SolarPassThroughEnabled || isBelowStopThreshold() - || !SolarCharger.isDataValid()) { + || !solarChargerOutput + ) { return 0; } - return SolarCharger.getOutputPowerWatts(); + return *solarChargerOutput; } float PowerLimiterClass::getBatteryInvertersOutputAcWatts() diff --git a/src/PowerLimiterSolarInverter.cpp b/src/PowerLimiterSolarInverter.cpp index 119e49663..58312232e 100644 --- a/src/PowerLimiterSolarInverter.cpp +++ b/src/PowerLimiterSolarInverter.cpp @@ -27,14 +27,6 @@ uint16_t PowerLimiterSolarInverter::getMaxIncreaseWatts() const // the maximum increase possible for this inverter int16_t maxTotalIncrease = getConfiguredMaxPowerWatts() - getCurrentOutputAcWatts(); - // when the current limit is less than 15% of the max power of the inverter - // the output will not match the limit as the inverters are not able to work - // with those low limits. In this case we assume that the inverter is able to - // provide more power and we return the maximum possible increase. - // thanks spcqike for creating a table that can be found here: - // https://github.com/hoylabs/OpenDTU-OnBattery/issues/1087#issuecomment-2216787552 - if (getCurrentLimitWatts() < getInverterMaxPowerWatts() * 0.15) { return maxTotalIncrease; } - auto pStats = _spInverter->Statistics(); std::vector dcMppts = _spInverter->getMppts(); size_t dcTotalMppts = dcMppts.size(); @@ -71,10 +63,8 @@ uint16_t PowerLimiterSolarInverter::getMaxIncreaseWatts() const } if (dcNonShadedMppts == 0) { - // all mppts are shaded according to our calculation. - // but because we can not be sure we assume that we can - // increase the power by 3% of the max power. - return getConfiguredMaxPowerWatts() / 33; + // all mppts are shaded, we can't increase the power + return 0; } if (dcNonShadedMppts == dcTotalMppts) { diff --git a/src/SolarCharger.cpp b/src/SolarCharger.cpp deleted file mode 100644 index ad2724bd6..000000000 --- a/src/SolarCharger.cpp +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "SolarCharger.h" -#include -#include -#include - -SolarChargerClass SolarCharger; - -void SolarChargerClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&SolarChargerClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - this->updateSettings(); -} - -void SolarChargerClass::updateSettings() -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - _upProvider->deinit(); - _upProvider = nullptr; - } - - auto const& config = Configuration.get(); - if (!config.SolarCharger.Enabled) { return; } - - bool verboseLogging = config.SolarCharger.VerboseLogging; - - switch (config.SolarCharger.Provider) { - case SolarChargerProviderType::VEDIRECT: - _upProvider = std::make_unique(); - break; - default: - MessageOutput.printf("[SolarCharger] Unknown provider: %d\r\n", config.SolarCharger.Provider); - return; - } - - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } -} - -void SolarChargerClass::loop() -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - _upProvider->loop(); - } -} - -size_t SolarChargerClass::controllerAmount() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->controllerAmount(); - } - - return 0; -} - -bool SolarChargerClass::isDataValid() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->isDataValid(); - } - - return false; -} - -uint32_t SolarChargerClass::getDataAgeMillis() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getDataAgeMillis(); - } - - return 0; -} - -uint32_t SolarChargerClass::getDataAgeMillis(size_t idx) const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getDataAgeMillis(idx); - } - - return 0; -} - - -// total output of all MPPT charge controllers in Watts -int32_t SolarChargerClass::getOutputPowerWatts() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getOutputPowerWatts(); - } - - return 0; -} - -// total panel input power of all MPPT charge controllers in Watts -int32_t SolarChargerClass::getPanelPowerWatts() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getPanelPowerWatts(); - } - - return 0; -} - -// sum of total yield of all MPPT charge controllers in kWh -float SolarChargerClass::getYieldTotal() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getYieldTotal(); - } - - return 0; -} - -// sum of today's yield of all MPPT charge controllers in kWh -float SolarChargerClass::getYieldDay() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getYieldDay(); - } - - return 0; -} - -// minimum of all MPPT charge controllers' output voltages in V -float SolarChargerClass::getOutputVoltage() const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getOutputVoltage(); - } - - return 0; -} - -std::optional SolarChargerClass::getData(size_t idx) const -{ - std::lock_guard lock(_mutex); - - if (_upProvider) { - return _upProvider->getData(idx); - } - - return std::nullopt; -} diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp deleted file mode 100644 index a6bc4f7a0..000000000 --- a/src/VictronMppt.cpp +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "VictronMppt.h" -#include "Configuration.h" -#include "PinMapping.h" -#include "MessageOutput.h" -#include "SerialPortManager.h" - -bool VictronMppt::init(bool verboseLogging) -{ - const PinMapping_t& pin = PinMapping.get(); - auto controllerCount = 0; - - if (initController(pin.victron_rx, pin.victron_tx, verboseLogging, 1)) { - controllerCount++; - } - - if (initController(pin.victron_rx2, pin.victron_tx2, verboseLogging, 2)) { - controllerCount++; - } - - if (initController(pin.victron_rx3, pin.victron_tx3, verboseLogging, 3)) { - controllerCount++; - } - - return controllerCount > 0; -} - -void VictronMppt::deinit() -{ - std::lock_guard lock(_mutex); - - _controllers.clear(); - for (auto const& o: _serialPortOwners) { - SerialPortManager.freePort(o.c_str()); - } - _serialPortOwners.clear(); -} - -bool VictronMppt::initController(int8_t rx, int8_t tx, bool logging, - uint8_t instance) -{ - MessageOutput.printf("[VictronMppt Instance %d] rx = %d, tx = %d\r\n", - instance, rx, tx); - - if (rx < 0) { - MessageOutput.printf("[VictronMppt Instance %d] invalid pin config\r\n", instance); - return false; - } - - String owner("Victron MPPT "); - owner += String(instance); - auto oHwSerialPort = SerialPortManager.allocatePort(owner.c_str()); - if (!oHwSerialPort) { return false; } - - _serialPortOwners.push_back(owner); - - auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, logging, *oHwSerialPort); - _controllers.push_back(std::move(upController)); - return true; -} - -void VictronMppt::loop() -{ - std::lock_guard lock(_mutex); - - for (auto const& upController : _controllers) { - upController->loop(); - } -} - -/* - * isDataValid() - * return: true = if at least one of the MPPT controllers delivers valid data - */ -bool VictronMppt::isDataValid() const -{ - std::lock_guard lock(_mutex); - - for (auto const& upController: _controllers) { - if (upController->isDataValid()) { return true; } - } - - return false; -} - -uint32_t VictronMppt::getDataAgeMillis() const -{ - std::lock_guard lock(_mutex); - - if (_controllers.empty()) { return 0; } - - auto now = millis(); - - auto iter = _controllers.cbegin(); - uint32_t age = now - (*iter)->getLastUpdate(); - ++iter; - - while (iter != _controllers.end()) { - age = std::min(age, now - (*iter)->getLastUpdate()); - ++iter; - } - - return age; -} - -uint32_t VictronMppt::getDataAgeMillis(size_t idx) const -{ - std::lock_guard lock(_mutex); - - if (_controllers.empty() || idx >= _controllers.size()) { return 0; } - - return millis() - _controllers[idx]->getLastUpdate(); -} - -std::optional VictronMppt::getData(size_t idx) const -{ - std::lock_guard lock(_mutex); - - if (_controllers.empty() || idx >= _controllers.size()) { - MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", - idx, _controllers.size()); - return std::nullopt; - } - - if (!_controllers[idx]->isDataValid()) { return std::nullopt; } - - return _controllers[idx]->getData(); -} - -int32_t VictronMppt::getOutputPowerWatts() const -{ - int32_t sum = 0; - - for (const auto& upController : _controllers) { - if (!upController->isDataValid()) { continue; } - - // if any charge controller is part of a VE.Smart network, and if the - // charge controller is connected in a way that allows to send - // requests, we should have the "network total DC input power" - // available. if so, to estimate the output power, we multiply by - // the calculated efficiency of the connected charge controller. - auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; - if (networkPower.first > 0) { - return static_cast(networkPower.second / 1000.0 * upController->getData().mpptEfficiency_Percent / 100); - } - - sum += upController->getData().batteryOutputPower_W; - } - - return sum; -} - -int32_t VictronMppt::getPanelPowerWatts() const -{ - int32_t sum = 0; - - for (const auto& upController : _controllers) { - if (!upController->isDataValid()) { continue; } - - // if any charge controller is part of a VE.Smart network, and if the - // charge controller is connected in a way that allows to send - // requests, we should have the "network total DC input power" available. - auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; - if (networkPower.first > 0) { - return static_cast(networkPower.second / 1000.0); - } - - sum += upController->getData().panelPower_PPV_W; - } - - return sum; -} - -float VictronMppt::getYieldTotal() const -{ - float sum = 0; - - for (const auto& upController : _controllers) { - if (!upController->isDataValid()) { continue; } - sum += upController->getData().yieldTotal_H19_Wh / 1000.0; - } - - return sum; -} - -float VictronMppt::getYieldDay() const -{ - float sum = 0; - - for (const auto& upController : _controllers) { - if (!upController->isDataValid()) { continue; } - sum += upController->getData().yieldToday_H20_Wh / 1000.0; - } - - return sum; -} - -float VictronMppt::getOutputVoltage() const -{ - float min = -1; - - for (const auto& upController : _controllers) { - if (!upController->isDataValid()) { continue; } - float volts = upController->getData().batteryVoltage_V_mV / 1000.0; - if (min == -1) { min = volts; } - min = std::min(min, volts); - } - - return min; -} - -std::optional VictronMppt::getStateOfOperation() const -{ - for (const auto& upController : _controllers) { - if (upController->isDataValid()) { - return upController->getData().currentState_CS; - } - } - - return std::nullopt; -} - -std::optional VictronMppt::getVoltage(MPPTVoltage kindOf) const -{ - for (const auto& upController : _controllers) { - switch (kindOf) { - case MPPTVoltage::ABSORPTION: { - auto const& absorptionVoltage = upController->getData().BatteryAbsorptionMilliVolt; - if (absorptionVoltage.first > 0) { return absorptionVoltage.second; } - break; - } - case MPPTVoltage::FLOAT: { - auto const& floatVoltage = upController->getData().BatteryFloatMilliVolt; - if (floatVoltage.first > 0) { return floatVoltage.second; } - break; - } - case MPPTVoltage::BATTERY: { - auto const& batteryVoltage = upController->getData().batteryVoltage_V_mV; - if (upController->isDataValid()) { return batteryVoltage; } - break; - } - } - } - - return std::nullopt; -} diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index c64f9a83a..1c5d567b3 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -37,7 +37,7 @@ void VictronSmartShunt::loop() { VeDirectShunt.loop(); - if (VeDirectShunt.getLastUpdate() <= _lastUpdate) { return; } + if (VeDirectShunt.getLastUpdate() == _lastUpdate) { return; } _stats->updateFrom(VeDirectShunt.getData()); _lastUpdate = VeDirectShunt.getLastUpdate(); diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index d55a5322b..c7f1f358e 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -160,10 +160,14 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } + bool performRestart = false; + { auto guard = Configuration.getWriteGuard(); auto& config = guard.getConfig(); + performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping; + strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as().c_str(), sizeof(config.Dev_PinMapping)); config.Display.Rotation = root["display"]["rotation"].as(); config.Display.PowerSafe = root["display"]["power_safe"].as(); @@ -180,7 +184,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) } auto const& config = Configuration.get(); - bool performRestart = root["curPin"]["name"].as() != config.Dev_PinMapping; Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.setOrientation(config.Display.Rotation); diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 1511ae590..a2a9205ee 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -5,11 +5,10 @@ #include "WebApi_firmware.h" #include "Configuration.h" #include "RestartHelper.h" -#include "Update.h" -#include "Utils.h" #include "WebApi.h" #include "helper.h" #include +#include #include "esp_partition.h" void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 1d0c2ab15..8727dd220 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -10,8 +10,6 @@ #include "MqttHandleInverter.h" #include "MqttHandleHuawei.h" #include "MqttHandlePowerLimiter.h" -#include "MqttHandleVedirectHass.h" -#include "MqttHandleVedirect.h" #include "MqttSettings.h" #include "WebApi.h" #include "WebApi_errors.h" @@ -19,6 +17,7 @@ #include "PowerLimiter.h" #include "PowerMeter.h" #include +#include void WebApiMqttClass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -255,11 +254,11 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - if (root["mqtt_publish_interval"].as() < 5 || root["mqtt_publish_interval"].as() > 65535) { - retMsg["message"] = "Publish interval must be a number between 5 and 65535!"; + if (root["mqtt_publish_interval"].as() < 1 || root["mqtt_publish_interval"].as() > 86400) { + retMsg["message"] = "Publish interval must be a number between 1 and 86400!"; retMsg["code"] = WebApiError::MqttPublishInterval; - retMsg["param"]["min"] = 5; - retMsg["param"]["max"] = 65535; + retMsg["param"]["min"] = 1; + retMsg["param"]["max"] = 86400; WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -334,11 +333,11 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) MqttHandleBatteryHass.forceUpdate(); MqttHandleHass.forceUpdate(); MqttHandlePowerLimiterHass.forceUpdate(); - MqttHandleVedirectHass.forceUpdate(); MqttHandleHuawei.forceUpdate(); MqttHandlePowerLimiter.forceUpdate(); - MqttHandleVedirect.forceUpdate(); + + SolarCharger.updateSettings(); } String WebApiMqttClass::getTlsCertInfo(const char* cert) diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index e691ad79d..35fc04dcd 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -6,7 +6,6 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" -#include "MqttHandleVedirectHass.h" #include "MqttHandleHass.h" #include "MqttSettings.h" #include "PowerLimiter.h" diff --git a/src/WebApi_solar_charger.cpp b/src/WebApi_solarcharger.cpp similarity index 88% rename from src/WebApi_solar_charger.cpp rename to src/WebApi_solarcharger.cpp index fcdb8aeba..72015b55d 100644 --- a/src/WebApi_solar_charger.cpp +++ b/src/WebApi_solarcharger.cpp @@ -7,7 +7,7 @@ #include "WebApi_errors.h" #include "helper.h" #include "MqttHandlePowerLimiterHass.h" -#include "SolarCharger.h" +#include void WebApiSolarChargerlass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -31,6 +31,9 @@ void WebApiSolarChargerlass::onAdminGet(AsyncWebServerRequest* request) ConfigurationClass::serializeSolarChargerConfig(config.SolarCharger, root); + auto mqtt = root["mqtt"].to(); + ConfigurationClass::serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, mqtt); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -62,6 +65,8 @@ void WebApiSolarChargerlass::onAdminPost(AsyncWebServerRequest* request) auto guard = Configuration.getWriteGuard(); auto& config = guard.getConfig(); ConfigurationClass::deserializeSolarChargerConfig(root.as(), config.SolarCharger); + + ConfigurationClass::deserializeSolarChargerMqttConfig(root["mqtt"].as(), config.SolarCharger.Mqtt); } WebApi.writeConfig(retMsg); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index fa1d223ba..1eddcb277 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -11,9 +11,13 @@ #include #include "PowerMeter.h" #include "defaults.h" -#include "SolarCharger.h" +#include #include +#ifndef PIN_MAPPING_REQUIRED + #define PIN_MAPPING_REQUIRED 0 +#endif + WebApiWsLiveClass::WebApiWsLiveClass() : _ws("/livedata") , _wsCleanupTask(1 * TASK_SECOND, TASK_FOREVER, std::bind(&WebApiWsLiveClass::wsCleanupTaskCb, this)) @@ -72,16 +76,35 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al auto const& config = Configuration.get(); auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; - auto solarChargerAge = SolarCharger.getDataAgeMillis(); + auto solarChargerAge = SolarCharger.getStats()->getAgeMillis(); if (all || (solarChargerAge > 0 && (millis() - _lastPublishSolarCharger) > solarChargerAge)) { auto solarchargerObj = root["solarcharger"].to(); solarchargerObj["enabled"] = config.SolarCharger.Enabled; if (config.SolarCharger.Enabled) { - auto totalVeObj = solarchargerObj["total"].to(); - addTotalField(totalVeObj, "Power", SolarCharger.getPanelPowerWatts(), "W", 1); - addTotalField(totalVeObj, "YieldDay", SolarCharger.getYieldDay() * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", SolarCharger.getYieldTotal(), "kWh", 2); + float power = 0; + auto outputPower = SolarCharger.getStats()->getOutputPowerWatts(); + auto panelPower = SolarCharger.getStats()->getPanelPowerWatts(); + + if (outputPower) { + power = *outputPower; + } + + if (power == 0 && panelPower) { + power = *panelPower; + } + + addTotalField(solarchargerObj, "power", power, "W", 1); + + auto yieldDay = SolarCharger.getStats()->getYieldDay(); + if (yieldDay) { + addTotalField(solarchargerObj, "yieldDay", *yieldDay, "Wh", 0); + } + + auto yieldTotal = SolarCharger.getStats()->getYieldTotal(); + if (yieldTotal) { + addTotalField(solarchargerObj, "yieldTotal", *yieldTotal, "kWh", 2); + } } if (!all) { _lastPublishSolarCharger = millis(); } @@ -224,8 +247,7 @@ void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; - bool isGeneric = std::string(PIOENV).find("generic") != std::string::npos; - hintObj["pin_mapping_issue"] = isGeneric && !PinMapping.isMappingSelected(); + hintObj["pin_mapping_issue"] = PIN_MAPPING_REQUIRED && !PinMapping.isMappingSelected(); } void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) diff --git a/src/WebApi_ws_solarcharger_live.cpp b/src/WebApi_ws_solarcharger_live.cpp index ed5595314..533877c4c 100644 --- a/src/WebApi_ws_solarcharger_live.cpp +++ b/src/WebApi_ws_solarcharger_live.cpp @@ -10,7 +10,7 @@ #include "WebApi.h" #include "defaults.h" #include "PowerLimiter.h" -#include "SolarCharger.h" +#include WebApiWsSolarChargerLiveClass::WebApiWsSolarChargerLiveClass() : _ws("/solarchargerlivedata") @@ -72,20 +72,6 @@ void WebApiWsSolarChargerLiveClass::wsCleanupTaskCb() _ws.cleanupClients(); } -bool WebApiWsSolarChargerLiveClass::hasUpdate(size_t idx) -{ - auto dataAgeMillis = SolarCharger.getDataAgeMillis(idx); - if (dataAgeMillis == 0) { return false; } - auto publishAgeMillis = millis() - _lastPublish; - return dataAgeMillis < publishAgeMillis; -} - -uint16_t WebApiWsSolarChargerLiveClass::responseSize() const -{ - // estimated with ArduinoJson assistant - return SolarCharger.controllerAmount() * (1024 + 512) + 128/*DPL status and structure*/; -} - void WebApiWsSolarChargerLiveClass::sendDataTaskCb() { // do nothing if no WS client is connected @@ -93,15 +79,9 @@ void WebApiWsSolarChargerLiveClass::sendDataTaskCb() // Update on ve.direct change or at least after 10 seconds bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000)); - bool updateAvailable = false; - if (!fullUpdate) { - for (size_t idx = 0; idx < SolarCharger.controllerAmount(); ++idx) { - if (hasUpdate(idx)) { - updateAvailable = true; - break; - } - } - } + + auto publishAgeMillis = millis() - _lastPublish; + bool updateAvailable = SolarCharger.getStats()->getAgeMillis() < publishAgeMillis; if (fullUpdate || updateAvailable) { try { @@ -131,127 +111,8 @@ void WebApiWsSolarChargerLiveClass::sendDataTaskCb() void WebApiWsSolarChargerLiveClass::generateCommonJsonResponse(JsonVariant& root, bool fullUpdate) { - auto array = root["solarcharger"]["instances"].to(); - root["solarcharger"]["full_update"] = fullUpdate; - - for (size_t idx = 0; idx < SolarCharger.controllerAmount(); ++idx) { - auto optMpptData = SolarCharger.getData(idx); - if (!optMpptData.has_value()) { continue; } - - if (!fullUpdate && !hasUpdate(idx)) { continue; } - - String serial(optMpptData->serialNr_SER); - if (serial.isEmpty()) { continue; } // serial required as index - - JsonObject nested = array[serial].to(); - nested["data_age_ms"] = SolarCharger.getDataAgeMillis(idx); - populateJson(nested, *optMpptData); - } - + SolarCharger.getStats()->getLiveViewData(root, fullUpdate, _lastPublish); _lastPublish = millis(); - - // power limiter state - root["dpl"]["PLSTATE"] = -1; - if (Configuration.get().PowerLimiter.Enabled) - root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); - root["dpl"]["PLLIMIT"] = PowerLimiter.getInverterOutput(); -} - -void WebApiWsSolarChargerLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { - root["product_id"] = mpptData.getPidAsString(); - root["firmware_version"] = mpptData.getFwVersionFormatted(); - - const JsonObject values = root["values"].to(); - - const JsonObject device = values["device"].to(); - - // LOAD IL UI label result - // ------------------------------------ - // false false Do not display LOAD and IL (device has no physical load output and virtual load is not configured) - // true false "VIRTLOAD" We display just LOAD (device has no physical load output and virtual load is configured) - // true true "LOAD" We display LOAD and IL (device has physical load output, regardless if virtual load is configured or not) - if (mpptData.loadOutputState_LOAD.first > 0) { - device[(mpptData.loadCurrent_IL_mA.first > 0) ? "LOAD" : "VIRTLOAD"] = mpptData.loadOutputState_LOAD.second ? "ON" : "OFF"; - } - if (mpptData.loadCurrent_IL_mA.first > 0) { - device["IL"]["v"] = mpptData.loadCurrent_IL_mA.second / 1000.0; - device["IL"]["u"] = "A"; - device["IL"]["d"] = 2; - } - device["CS"] = mpptData.getCsAsString(); - device["MPPT"] = mpptData.getMpptAsString(); - device["OR"] = mpptData.getOrAsString(); - if (mpptData.relayState_RELAY.first > 0) { - device["RELAY"] = mpptData.relayState_RELAY.second ? "ON" : "OFF"; - } - device["ERR"] = mpptData.getErrAsString(); - device["HSDS"]["v"] = mpptData.daySequenceNr_HSDS; - device["HSDS"]["u"] = "d"; - if (mpptData.MpptTemperatureMilliCelsius.first > 0) { - device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0; - device["MpptTemperature"]["u"] = "°C"; - device["MpptTemperature"]["d"] = "1"; - } - - const JsonObject output = values["output"].to(); - output["P"]["v"] = mpptData.batteryOutputPower_W; - output["P"]["u"] = "W"; - output["P"]["d"] = 0; - output["V"]["v"] = mpptData.batteryVoltage_V_mV / 1000.0; - output["V"]["u"] = "V"; - output["V"]["d"] = 2; - output["I"]["v"] = mpptData.batteryCurrent_I_mA / 1000.0; - output["I"]["u"] = "A"; - output["I"]["d"] = 2; - output["E"]["v"] = mpptData.mpptEfficiency_Percent; - output["E"]["u"] = "%"; - output["E"]["d"] = 1; - if (mpptData.SmartBatterySenseTemperatureMilliCelsius.first > 0) { - output["SBSTemperature"]["v"] = mpptData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0; - output["SBSTemperature"]["u"] = "°C"; - output["SBSTemperature"]["d"] = "0"; - } - if (mpptData.BatteryAbsorptionMilliVolt.first > 0) { - output["AbsorptionVoltage"]["v"] = mpptData.BatteryAbsorptionMilliVolt.second / 1000.0; - output["AbsorptionVoltage"]["u"] = "V"; - output["AbsorptionVoltage"]["d"] = "2"; - } - if (mpptData.BatteryFloatMilliVolt.first > 0) { - output["FloatVoltage"]["v"] = mpptData.BatteryFloatMilliVolt.second / 1000.0; - output["FloatVoltage"]["u"] = "V"; - output["FloatVoltage"]["d"] = "2"; - } - - const JsonObject input = values["input"].to(); - if (mpptData.NetworkTotalDcInputPowerMilliWatts.first > 0) { - input["NetworkPower"]["v"] = mpptData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0; - input["NetworkPower"]["u"] = "W"; - input["NetworkPower"]["d"] = "0"; - } - input["PPV"]["v"] = mpptData.panelPower_PPV_W; - input["PPV"]["u"] = "W"; - input["PPV"]["d"] = 0; - input["VPV"]["v"] = mpptData.panelVoltage_VPV_mV / 1000.0; - input["VPV"]["u"] = "V"; - input["VPV"]["d"] = 2; - input["IPV"]["v"] = mpptData.panelCurrent_mA / 1000.0; - input["IPV"]["u"] = "A"; - input["IPV"]["d"] = 2; - input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh / 1000.0; - input["YieldToday"]["u"] = "kWh"; - input["YieldToday"]["d"] = 2; - input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh / 1000.0; - input["YieldYesterday"]["u"] = "kWh"; - input["YieldYesterday"]["d"] = 2; - input["YieldTotal"]["v"] = mpptData.yieldTotal_H19_Wh / 1000.0; - input["YieldTotal"]["u"] = "kWh"; - input["YieldTotal"]["d"] = 2; - input["MaximumPowerToday"]["v"] = mpptData.maxPowerToday_H21_W; - input["MaximumPowerToday"]["u"] = "W"; - input["MaximumPowerToday"]["d"] = 0; - input["MaximumPowerYesterday"]["v"] = mpptData.maxPowerYesterday_H23_W; - input["MaximumPowerYesterday"]["u"] = "W"; - input["MaximumPowerYesterday"]["d"] = 0; } void WebApiWsSolarChargerLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) diff --git a/src/gridcharger/huawei/Controller.cpp b/src/gridcharger/huawei/Controller.cpp index 2a4e24178..6359b2e66 100644 --- a/src/gridcharger/huawei/Controller.cpp +++ b/src/gridcharger/huawei/Controller.cpp @@ -377,8 +377,15 @@ void Controller::getJsonData(JsonVariant& root) const VAL(OutputPower, "output_power"); VAL(InputTemperature, "input_temp"); VAL(OutputTemperature, "output_temp"); - VAL(Efficiency, "efficiency"); #undef VAL + + // special handling for efficiency, as we need to multiply it + // to get the percentage (rather than the decimal notation). + auto oEfficiency = _dataPoints.getDataPointFor(); + if (oEfficiency) { + root["efficiency"]["v"] = *_dataPoints.get() * 100; + root["efficiency"]["u"] = oEfficiency->getUnitText(); + } } } // namespace GridCharger::Huawei diff --git a/src/gridcharger/huawei/HardwareInterface.cpp b/src/gridcharger/huawei/HardwareInterface.cpp index aee35ad65..2bb3a5bec 100644 --- a/src/gridcharger/huawei/HardwareInterface.cpp +++ b/src/gridcharger/huawei/HardwareInterface.cpp @@ -31,7 +31,7 @@ bool HardwareInterface::startLoop() { uint32_t constexpr stackSize = 3072; return pdPASS == xTaskCreate(HardwareInterface::staticLoopHelper, - "HuaweiHwIfc", stackSize, this, 10/*prio*/, &_taskHandle); + "HuaweiHwIfc", stackSize, this, 16/*prio*/, &_taskHandle); } void HardwareInterface::stopLoop() @@ -108,6 +108,13 @@ void HardwareInterface::loop() _upDataInFlight->add(value); break; } + + // the OutputCurent value is the last value in a data request's answer + // among all values we process into the data point container, so we + // make the in-flight container the current container. + if (label == DataPointLabel::OutputCurrent) { + _upDataCurrent = std::move(_upDataInFlight); + } } size_t queueSize = _sendQueue.size(); @@ -135,20 +142,12 @@ void HardwareInterface::loop() _nextRequestMillis = millis() + DataRequestIntervalMillis; - auto const& config = Configuration.get(); - if (_upDataInFlight && config.Huawei.VerboseLogging) { - auto iter = _upDataInFlight->cbegin(); - while (iter != _upDataInFlight->cend()) { - MessageOutput.printf("[Huawei::HwIfc] [%.3f] %s: %s%s\r\n", - static_cast(iter->second.getTimestamp())/1000, - iter->second.getLabelText().c_str(), - iter->second.getValueText().c_str(), - iter->second.getUnitText().c_str()); - ++iter; - } + // this should be redundant, as every answer to a data request should + // have the OutputCurrent value, which is supposed to be the last value + // in the answer, and it already triggers moving the data in flight. + if (_upDataInFlight) { + _upDataCurrent = std::move(_upDataInFlight); } - - _upDataCurrent = std::move(_upDataInFlight); } } @@ -170,8 +169,34 @@ void HardwareInterface::setParameter(HardwareInterface::Setting setting, float v } _sendQueue.push({setting, static_cast(val)}); + _nextRequestMillis = millis() - 1; // request param feedback immediately xTaskNotifyGive(_taskHandle); } +std::unique_ptr HardwareInterface::getCurrentData() +{ + std::unique_ptr upData = nullptr; + + { + std::lock_guard lock(_mutex); + upData = std::move(_upDataCurrent); + } + + auto const& config = Configuration.get(); + if (upData && config.Huawei.VerboseLogging) { + auto iter = upData->cbegin(); + while (iter != upData->cend()) { + MessageOutput.printf("[Huawei::HwIfc] [%.3f] %s: %s%s\r\n", + static_cast(iter->second.getTimestamp())/1000, + iter->second.getLabelText().c_str(), + iter->second.getValueText().c_str(), + iter->second.getUnitText().c_str()); + ++iter; + } + } + + return std::move(upData); +} + } // namespace GridCharger::Huawei diff --git a/src/gridcharger/huawei/MCP2515.cpp b/src/gridcharger/huawei/MCP2515.cpp index 5265c9d84..d55ba5c2f 100644 --- a/src/gridcharger/huawei/MCP2515.cpp +++ b/src/gridcharger/huawei/MCP2515.cpp @@ -16,8 +16,11 @@ void mcp2515Isr() if (sIsrTaskHandle == nullptr) { return; } BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(sIsrTaskHandle, &xHigherPriorityTaskWoken); - // we assume we can wait until the lower-priority task is scheduled - // anyways, so we ignore xHigherPriorityTaskWoken == pdTRUE. + // make sure that the high-priority hardware interface task is scheduled, + // as the timing is very critical. CAN messages will be missed if the + // MCP2515 interrupt is not serviced immediately, as a new message + // overwrites a pending message. + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } std::optional MCP2515::_oSpiBus = std::nullopt; diff --git a/src/main.cpp b/src/main.cpp index 1c0dd4b36..ba12c129d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,11 +14,9 @@ #include #include "MqttHandleDtu.h" #include "MqttHandleHass.h" -#include "MqttHandleVedirectHass.h" #include "MqttHandleBatteryHass.h" #include "MqttHandleInverter.h" #include "MqttHandleInverterTotal.h" -#include "MqttHandleVedirect.h" #include "MqttHandleHuawei.h" #include "MqttHandlePowerLimiter.h" #include "MqttHandlePowerLimiterHass.h" @@ -34,7 +32,7 @@ #include "PowerMeter.h" #include "PowerLimiter.h" #include "defaults.h" -#include "SolarCharger.h" +#include #include #include #include @@ -104,7 +102,7 @@ void setup() // Load PinMapping MessageOutput.print("Reading PinMapping... "); - if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { + if (PinMapping.init(Configuration.get().Dev_PinMapping)) { MessageOutput.print("found valid mapping "); } else { MessageOutput.print("using default config "); @@ -114,7 +112,7 @@ void setup() SerialPortManager.init(); - // Initialize WiFi + // Initialize Network MessageOutput.print("Initialize Network... "); NetworkSettings.init(scheduler); MessageOutput.println("done"); @@ -136,9 +134,7 @@ void setup() MqttHandleDtu.init(scheduler); MqttHandleInverter.init(scheduler); MqttHandleInverterTotal.init(scheduler); - MqttHandleVedirect.init(scheduler); MqttHandleHass.init(scheduler); - MqttHandleVedirectHass.init(scheduler); MqttHandleBatteryHass.init(scheduler); MqttHandleHuawei.init(scheduler); MqttHandlePowerLimiter.init(scheduler); diff --git a/src/solarcharger/Controller.cpp b/src/solarcharger/Controller.cpp new file mode 100644 index 000000000..1efc62d09 --- /dev/null +++ b/src/solarcharger/Controller.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include +#include +#include + +SolarChargers::Controller SolarCharger; + +namespace SolarChargers { + +void Controller::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&Controller::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + this->updateSettings(); +} + +void Controller::updateSettings() +{ + std::lock_guard lock(_mutex); + + if (_upProvider) { + _upProvider->deinit(); + _upProvider = nullptr; + } + + auto const& config = Configuration.get(); + if (!config.SolarCharger.Enabled) { return; } + + bool verboseLogging = config.SolarCharger.VerboseLogging; + + switch (config.SolarCharger.Provider) { + case SolarChargerProviderType::VEDIRECT: + _upProvider = std::make_unique<::SolarChargers::Victron::Provider>(); + break; + case SolarChargerProviderType::MQTT: + _upProvider = std::make_unique<::SolarChargers::Mqtt::Provider>(); + break; + default: + MessageOutput.printf("[SolarCharger] Unknown provider: %d\r\n", config.SolarCharger.Provider); + return; + } + + if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } + + _forcePublishSensors = true; +} + +std::shared_ptr Controller::getStats() const +{ + std::lock_guard lock(_mutex); + + if (!_upProvider) { + static auto sspDummyStats = std::make_shared(); + return sspDummyStats; + } + + return _upProvider->getStats(); +} + +void Controller::loop() +{ + std::lock_guard lock(_mutex); + + if (!_upProvider) { return; } + + _upProvider->loop(); + + // TODO(schlimmchen): this cannot make sure that transient + // connection problems are actually always noticed. + if (!MqttSettings.getConnected()) { + _forcePublishSensors = true; + return; + } + + _upProvider->getStats()->mqttLoop(); + + auto const& config = Configuration.get(); + if (!config.Mqtt.Hass.Enabled) { return; } + + _upProvider->getStats()->mqttPublishSensors(_forcePublishSensors); + + _forcePublishSensors = false; +} + +} // namespace SolarChargers diff --git a/src/solarcharger/HassIntegration.cpp b/src/solarcharger/HassIntegration.cpp new file mode 100644 index 000000000..241ca68e3 --- /dev/null +++ b/src/solarcharger/HassIntegration.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include <__compiled_constants.h> + +namespace SolarChargers { + +void HassIntegration::publish(const String& subtopic, const String& payload) const +{ + String topic = Configuration.get().Mqtt.Hass.Topic; + topic += subtopic; + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); +} + +} // namespace SolarChargers diff --git a/src/solarcharger/Stats.cpp b/src/solarcharger/Stats.cpp new file mode 100644 index 000000000..56177be1f --- /dev/null +++ b/src/solarcharger/Stats.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include + +namespace SolarChargers { + +void Stats::getLiveViewData(JsonVariant& root, boolean fullUpdate, uint32_t lastPublish) const +{ + // power limiter state + root["dpl"]["PLSTATE"] = -1; + if (Configuration.get().PowerLimiter.Enabled) { + root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); + } + root["dpl"]["PLLIMIT"] = PowerLimiter.getInverterOutput(); + + root["solarcharger"]["full_update"] = fullUpdate; +} + +void Stats::mqttLoop() +{ + auto& config = Configuration.get(); + + if (!MqttSettings.getConnected() + || (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) { + return; + } + + mqttPublish(); + + _lastMqttPublish = millis(); +} + +uint32_t Stats::getMqttFullPublishIntervalMs() const +{ + auto& config = Configuration.get(); + + // this is the default interval, see mqttLoop(). mqttPublish() + // implementations in derived classes may choose to publish some values + // with a lower frequency and hence implement this method with a different + // return value. + return config.Mqtt.PublishInterval * 1000; +} + +void Stats::mqttPublish() const +{ +} + +} // namespace SolarChargers diff --git a/src/solarcharger/mqtt/Provider.cpp b/src/solarcharger/mqtt/Provider.cpp new file mode 100644 index 000000000..4810cf354 --- /dev/null +++ b/src/solarcharger/mqtt/Provider.cpp @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include + +namespace SolarChargers::Mqtt { + +bool Provider::init(bool verboseLogging) +{ + _verboseLogging = verboseLogging; + auto const& config = Configuration.get().SolarCharger.Mqtt; + + _outputPowerTopic = config.PowerTopic; + _outputCurrentTopic = config.CurrentTopic; + _outputVoltageTopic = config.VoltageTopic; + + bool configValid = !config.CalculateOutputPower && !_outputPowerTopic.isEmpty(); + if (config.CalculateOutputPower) { + configValid = !_outputCurrentTopic.isEmpty() && !_outputVoltageTopic.isEmpty(); + } + + if (!configValid) { + MessageOutput.printf("[SolarChargers::Mqtt]: Init failed. " + "switch 'calculate output power' %s, power topic %s, " + "current topic %s, voltage topic %s\r\n", + config.CalculateOutputPower ? "enabled" : "disabled", + _outputPowerTopic.isEmpty() ? "empty" : "available", + _outputCurrentTopic.isEmpty() ? "empty" : "available", + _outputVoltageTopic.isEmpty() ? "empty" : "available" + ); + return false; + } + + if (!_outputPowerTopic.isEmpty() + && !config.CalculateOutputPower) { + _subscribedTopics.push_back(_outputPowerTopic); + + MqttSettings.subscribe(_outputPowerTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputPower, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.PowerJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for ouput_power readings\r\n", + _outputPowerTopic.c_str()); + } + } + + if (!_outputCurrentTopic.isEmpty()) { + _subscribedTopics.push_back(_outputCurrentTopic); + + MqttSettings.subscribe(_outputCurrentTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputCurrent, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.CurrentJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for output_current readings\r\n", + _outputCurrentTopic.c_str()); + } + } + + if (!_outputVoltageTopic.isEmpty()) { + _subscribedTopics.push_back(_outputVoltageTopic); + + MqttSettings.subscribe(_outputVoltageTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputVoltage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.VoltageJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for ouput_voltage readings\r\n", + _outputVoltageTopic.c_str()); + } + } + + return true; +} + +void Provider::deinit() +{ + for (auto const& topic : _subscribedTopics) { + MqttSettings.unsubscribe(topic); + } + _subscribedTopics.clear(); +} + +void Provider::onMqttMessageOutputPower(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputPower = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputPower.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::WattageUnit; + switch (config.PowerUnit) { + case Unit_t::MilliWatts: + *outputPower /= 1000; + break; + case Unit_t::KiloWatts: + *outputPower *= 1000; + break; + default: + break; + } + + if (*outputPower < 0) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_power '%.1f' in topic '%s'\r\n", + *outputPower, topic); + return; + } + + _stats->setOutputPowerWatts(*outputPower); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_power to %.1f from '%s'\r\n", + *outputPower, topic); + } +} + +void Provider::onMqttMessageOutputVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputVoltage = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputVoltage.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::VoltageUnit; + switch (config.VoltageTopicUnit) { + case Unit_t::DeciVolts: + *outputVoltage /= 10; + break; + case Unit_t::CentiVolts: + *outputVoltage /= 100; + break; + case Unit_t::MilliVolts: + *outputVoltage /= 1000; + break; + default: + break; + } + + // since this project is revolving around Hoymiles microinverters, which can + // only handle up to 65V of input voltage at best, it is safe to assume that + // an even higher voltage is implausible. + if (*outputVoltage < 0 || *outputVoltage > 65) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_voltage '%.2f' in topic '%s'\r\n", + *outputVoltage, topic); + return; + } + + _stats->setOutputVoltage(*outputVoltage); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_voltage to %.2f from '%s'\r\n", + *outputVoltage, topic); + } +} + +void Provider::onMqttMessageOutputCurrent(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputCurrent = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputCurrent.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::AmperageUnit; + switch (config.CurrentUnit) { + case Unit_t::MilliAmps: + *outputCurrent /= 1000; + break; + default: + break; + } + + _stats->setOutputCurrent(*outputCurrent); + + if (*outputCurrent < 0) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_current '%.2f' in topic '%s'\r\n", + *outputCurrent, topic); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_current to %.2f from '%s'\r\n", + *outputCurrent, topic); + } +} + +} // namespace SolarChargers::Mqtt diff --git a/src/solarcharger/mqtt/Stats.cpp b/src/solarcharger/mqtt/Stats.cpp new file mode 100644 index 000000000..28af4b674 --- /dev/null +++ b/src/solarcharger/mqtt/Stats.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + +namespace SolarChargers::Mqtt { + +std::optional Stats::getOutputPowerWatts() const +{ + return getValueIfNotOutdated(_lastUpdateOutputPowerWatts, _outputPowerWatts); +} + +std::optional Stats::getOutputVoltage() const +{ + return getValueIfNotOutdated(_lastUpdateOutputVoltage, _outputVoltage); +} + +std::optional Stats::getOutputCurrent() const { + return getValueIfNotOutdated(_lastUpdateOutputCurrent, _outputCurrent); +} + +void Stats::setOutputVoltage(const float voltage) { + _outputVoltage = voltage; + _lastUpdateOutputVoltage = _lastUpdate = millis(); + + auto outputCurrent = getOutputCurrent(); + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower + && outputCurrent) { + setOutputPowerWatts(voltage * *outputCurrent); + } +} + +void Stats::setOutputCurrent(const float current) { + _outputCurrent = current; + _lastUpdateOutputCurrent = _lastUpdate = millis(); + + auto outputVoltage = getOutputVoltage(); + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower + && outputVoltage) { + setOutputPowerWatts(*outputVoltage * current); + } +} + +std::optional Stats::getValueIfNotOutdated(const uint32_t lastUpdate, const float value) const { + // never updated or older than 60 seconds + if (lastUpdate == 0 + || millis() - lastUpdate > 60 * 1000) { + return std::nullopt; + } + + return value; +} + +void Stats::getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const +{ + ::SolarChargers::Stats::getLiveViewData(root, fullUpdate, lastPublish); + + auto age = millis() - _lastUpdate; + + auto hasUpdate = _lastUpdate > 0 && age < millis() - lastPublish; + if (!fullUpdate && !hasUpdate) { return; } + + const JsonObject instance = root["solarcharger"]["instances"]["MQTT"].to(); + instance["data_age_ms"] = age; + instance["hide_serial"] = true; + instance["product_id"] = "MQTT"; // will be translated by the web app + + const JsonObject output = instance["values"]["output"].to(); + + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower) { + output["P"]["v"] = _outputPowerWatts; + output["P"]["u"] = "W"; + output["P"]["d"] = 1; + output["V"]["v"] = _outputVoltage; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + output["I"]["v"] = _outputCurrent; + output["I"]["u"] = "A"; + output["I"]["d"] = 2; + } + else { + output["Power"]["v"] = _outputPowerWatts; + output["Power"]["u"] = "W"; + output["Power"]["d"] = 1; + + auto outputVoltage = getOutputVoltage(); + if (outputVoltage) { + output["V"]["v"] = *outputVoltage; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + } + } +} + +}; // namespace SolarChargers::Mqtt diff --git a/src/solarcharger/victron/HassIntegration.cpp b/src/solarcharger/victron/HassIntegration.cpp new file mode 100644 index 000000000..55e2dedaa --- /dev/null +++ b/src/solarcharger/victron/HassIntegration.cpp @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "Configuration.h" +#include "MqttSettings.h" +#include "MqttHandleHass.h" +#include "Utils.h" +#include "__compiled_constants.h" +#include + +namespace SolarChargers::Victron { + +void HassIntegration::publishSensors(const VeDirectMpptController::data_t &mpptData) const +{ + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT firmware version integer", "mdi:counter", "FWI", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT firmware version formatted", "mdi:counter", "FWF", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT firmware version FW", "mdi:counter", "FW", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT firmware version FWE", "mdi:counter", "FWE", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, mpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", mpptData); + + // battery info + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", mpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", mpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", mpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", mpptData); + + // panel info + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", mpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", mpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", mpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", mpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", mpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", mpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", mpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", mpptData); + + // optional info, provided only if the charge controller delivers the information + if (mpptData.relayState_RELAY.first != 0) { + publishBinarySensor("MPPT error relay state", "mdi:electric-switch", "RELAY", "ON", "OFF", mpptData); + } + if (mpptData.loadOutputState_LOAD.first != 0) { + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", mpptData); + } + if (mpptData.loadCurrent_IL_mA.first != 0) { + publishSensor("MPPT load current", NULL, "IL", "current", "measurement", "A", mpptData); + } + + // optional info, provided only if TX is connected to charge controller + if (mpptData.NetworkTotalDcInputPowerMilliWatts.first != 0) { + publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", mpptData); + } + if (mpptData.MpptTemperatureMilliCelsius.first != 0) { + publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "°C", mpptData); + } + if (mpptData.BatteryAbsorptionMilliVolt.first != 0) { + publishSensor("Battery absorption voltage", "mdi:battery-charging-90", "BatteryAbsorption", "voltage", "measurement", "V", mpptData); + } + if (mpptData.BatteryFloatMilliVolt.first != 0) { + publishSensor("Battery float voltage", "mdi:battery-charging-100", "BatteryFloat", "voltage", "measurement", "V", mpptData); + } + if (mpptData.SmartBatterySenseTemperatureMilliCelsius.first != 0) { + publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "°C", mpptData); + } +} + +void HassIntegration::publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::data_t &mpptData) const +{ + String serial = mpptData.serialNr_SER; + + String sensorId = caption; + sensorId.replace(" ", "_"); + sensorId.replace(".", ""); + sensorId.replace("(", ""); + sensorId.replace(")", ""); + sensorId.toLowerCase(); + + String configTopic = "sensor/dtu_victron_" + serial + + "/" + sensorId + + "/config"; + + String statTopic = MqttSettings.getPrefix() + "victron/"; + statTopic.concat(serial); + statTopic.concat("/"); + statTopic.concat(subTopic); + + JsonDocument root; + + root["name"] = caption; + root["stat_t"] = statTopic; + root["uniq_id"] = serial + "_" + sensorId; + + if (icon != NULL) { + root["icon"] = icon; + } + + if (unitOfMeasurement != NULL) { + root["unit_of_meas"] = unitOfMeasurement; + } + + JsonObject deviceObj = root["dev"].to(); + createDeviceInfo(deviceObj, mpptData); + + if (Configuration.get().Mqtt.Hass.Expire) { + root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; + } + if (deviceClass != NULL) { + root["dev_cla"] = deviceClass; + } + if (stateClass != NULL) { + root["stat_cla"] = stateClass; + } + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + char buffer[512]; + serializeJson(root, buffer); + publish(configTopic, buffer); + +} + +void HassIntegration::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::data_t &mpptData) const +{ + String serial = mpptData.serialNr_SER; + + String sensorId = caption; + sensorId.replace(" ", "_"); + sensorId.replace(".", ""); + sensorId.replace("(", ""); + sensorId.replace(")", ""); + sensorId.toLowerCase(); + + String configTopic = "binary_sensor/dtu_victron_" + serial + + "/" + sensorId + + "/config"; + + String statTopic = MqttSettings.getPrefix() + "victron/"; + statTopic.concat(serial); + statTopic.concat("/"); + statTopic.concat(subTopic); + + JsonDocument root; + root["name"] = caption; + root["uniq_id"] = serial + "_" + sensorId; + root["stat_t"] = statTopic; + root["pl_on"] = payload_on; + root["pl_off"] = payload_off; + + if (icon != NULL) { + root["icon"] = icon; + } + + JsonObject deviceObj = root["dev"].to(); + createDeviceInfo(deviceObj, mpptData); + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + char buffer[512]; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void HassIntegration::createDeviceInfo(JsonObject &object, + const VeDirectMpptController::data_t &mpptData) const +{ + String serial = mpptData.serialNr_SER; + object["name"] = "Victron(" + serial + ")"; + object["ids"] = serial; + object["cu"] = MqttHandleHass.getDtuUrl(); + object["mf"] = "OpenDTU"; + object["mdl"] = mpptData.getPidAsString(); + object["sw"] = __COMPILED_GIT_HASH__; + object["via_device"] = MqttHandleHass.getDtuUniqueId(); +} + +} // namespace SolarChargers::Victron diff --git a/src/solarcharger/victron/Provider.cpp b/src/solarcharger/victron/Provider.cpp new file mode 100644 index 000000000..ab2f69987 --- /dev/null +++ b/src/solarcharger/victron/Provider.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "SerialPortManager.h" + +namespace SolarChargers::Victron { + +bool Provider::init(bool verboseLogging) +{ + const PinMapping_t& pin = PinMapping.get(); + auto controllerCount = 0; + + if (initController(pin.victron_rx, pin.victron_tx, verboseLogging, 1)) { + controllerCount++; + } + + if (initController(pin.victron_rx2, pin.victron_tx2, verboseLogging, 2)) { + controllerCount++; + } + + if (initController(pin.victron_rx3, pin.victron_tx3, verboseLogging, 3)) { + controllerCount++; + } + + return controllerCount > 0; +} + +void Provider::deinit() +{ + std::lock_guard lock(_mutex); + + _controllers.clear(); + for (auto const& o: _serialPortOwners) { + SerialPortManager.freePort(o.c_str()); + } + _serialPortOwners.clear(); +} + +bool Provider::initController(int8_t rx, int8_t tx, bool logging, + uint8_t instance) +{ + MessageOutput.printf("[VictronMppt Instance %d] rx = %d, tx = %d\r\n", + instance, rx, tx); + + if (rx < 0) { + MessageOutput.printf("[VictronMppt Instance %d] invalid pin config\r\n", instance); + return false; + } + + String owner("Victron MPPT "); + owner += String(instance); + auto oHwSerialPort = SerialPortManager.allocatePort(owner.c_str()); + if (!oHwSerialPort) { return false; } + + _serialPortOwners.push_back(owner); + + auto upController = std::make_unique(); + upController->init(rx, tx, &MessageOutput, logging, *oHwSerialPort); + _controllers.push_back(std::move(upController)); + return true; +} + +void Provider::loop() +{ + std::lock_guard lock(_mutex); + + for (auto const& upController : _controllers) { + upController->loop(); + + if(upController->isDataValid()) { + _stats->update(upController->getData().serialNr_SER, upController->getData(), upController->getLastUpdate()); + } else { + _stats->update(upController->getData().serialNr_SER, std::nullopt, upController->getLastUpdate()); + } + } +} + +} // namespace SolarChargers::Victron diff --git a/src/solarcharger/victron/Stats.cpp b/src/solarcharger/victron/Stats.cpp new file mode 100644 index 000000000..3f968f560 --- /dev/null +++ b/src/solarcharger/victron/Stats.cpp @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include + +namespace SolarChargers::Victron { + +void Stats::update(const String serial, const std::optional mpptData, uint32_t lastUpdate) const +{ + // serial required as index + if (serial.isEmpty()) { return; } + + _data[serial] = mpptData; + _lastUpdate[serial] = lastUpdate; +} + +uint32_t Stats::getAgeMillis() const +{ + uint32_t age = 0; + auto now = millis(); + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + if (!_lastUpdate[entry.first]) { continue; } + + age = std::max(age, now - _lastUpdate[entry.first]); + } + + return age; + +} + +std::optional Stats::getOutputPowerWatts() const +{ + float sum = 0; + auto data = false; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + data = true; + sum += entry.second->batteryOutputPower_W; + } + + if (!data) { return std::nullopt; } + + return sum; +} + +std::optional Stats::getOutputVoltage() const +{ + float min = -1; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + + float volts = entry.second->batteryVoltage_V_mV / 1000.0; + if (min == -1) { min = volts; } + min = std::min(min, volts); + } + + if (min == -1) { return std::nullopt; } + + return min; +} + +std::optional Stats::getPanelPowerWatts() const +{ + uint16_t sum = 0; + auto data = false; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + data = true; + + // if any charge controller is part of a VE.Smart network, and if the + // charge controller is connected in a way that allows to send + // requests, we should have the "network total DC input power" available. + auto networkPower = entry.second->NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0); + } + + sum += entry.second->panelPower_PPV_W; + } + + if (!data) { return std::nullopt; } + + return sum; +} + +std::optional Stats::getYieldTotal() const +{ + float sum = 0; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + + sum += entry.second->yieldTotal_H19_Wh / 1000.0; + } + + return sum; +} + +std::optional Stats::getYieldDay() const +{ + float sum = 0; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + + sum += entry.second->yieldToday_H20_Wh; + } + + return sum; +} + +void Stats::getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const +{ + ::SolarChargers::Stats::getLiveViewData(root, fullUpdate, lastPublish); + + auto instances = root["solarcharger"]["instances"].to(); + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + + auto age = 0; + if (_lastUpdate[entry.first]) { + age = millis() - _lastUpdate[entry.first]; + } + + auto hasUpdate = age != 0 && age < millis() - lastPublish; + if (!fullUpdate && !hasUpdate) { continue; } + + JsonObject instance = instances[entry.first].to(); + instance["data_age_ms"] = age; + instance["hide_serial"] = false; + populateJsonWithInstanceStats(instance, *entry.second); + } +} + +void Stats::populateJsonWithInstanceStats(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) const +{ + root["product_id"] = mpptData.getPidAsString(); + root["firmware_version"] = mpptData.getFwVersionFormatted(); + + const JsonObject values = root["values"].to(); + + const JsonObject device = values["device"].to(); + + // LOAD IL UI label result + // ------------------------------------ + // false false Do not display LOAD and IL (device has no physical load output and virtual load is not configured) + // true false "VIRTLOAD" We display just LOAD (device has no physical load output and virtual load is configured) + // true true "LOAD" We display LOAD and IL (device has physical load output, regardless if virtual load is configured or not) + if (mpptData.loadOutputState_LOAD.first > 0) { + device[(mpptData.loadCurrent_IL_mA.first > 0) ? "LOAD" : "VIRTLOAD"] = mpptData.loadOutputState_LOAD.second ? "ON" : "OFF"; + } + if (mpptData.loadCurrent_IL_mA.first > 0) { + device["IL"]["v"] = mpptData.loadCurrent_IL_mA.second / 1000.0; + device["IL"]["u"] = "A"; + device["IL"]["d"] = 2; + } + device["CS"] = mpptData.getCsAsString(); + device["MPPT"] = mpptData.getMpptAsString(); + device["OR"] = mpptData.getOrAsString(); + if (mpptData.relayState_RELAY.first > 0) { + device["RELAY"] = mpptData.relayState_RELAY.second ? "ON" : "OFF"; + } + device["ERR"] = mpptData.getErrAsString(); + device["HSDS"]["v"] = mpptData.daySequenceNr_HSDS; + device["HSDS"]["u"] = "d"; + if (mpptData.MpptTemperatureMilliCelsius.first > 0) { + device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0; + device["MpptTemperature"]["u"] = "°C"; + device["MpptTemperature"]["d"] = "1"; + } + + const JsonObject output = values["output"].to(); + output["P"]["v"] = mpptData.batteryOutputPower_W; + output["P"]["u"] = "W"; + output["P"]["d"] = 0; + output["V"]["v"] = mpptData.batteryVoltage_V_mV / 1000.0; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + output["I"]["v"] = mpptData.batteryCurrent_I_mA / 1000.0; + output["I"]["u"] = "A"; + output["I"]["d"] = 2; + output["E"]["v"] = mpptData.mpptEfficiency_Percent; + output["E"]["u"] = "%"; + output["E"]["d"] = 1; + if (mpptData.SmartBatterySenseTemperatureMilliCelsius.first > 0) { + output["SBSTemperature"]["v"] = mpptData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0; + output["SBSTemperature"]["u"] = "°C"; + output["SBSTemperature"]["d"] = "0"; + } + if (mpptData.BatteryAbsorptionMilliVolt.first > 0) { + output["AbsorptionVoltage"]["v"] = mpptData.BatteryAbsorptionMilliVolt.second / 1000.0; + output["AbsorptionVoltage"]["u"] = "V"; + output["AbsorptionVoltage"]["d"] = "2"; + } + if (mpptData.BatteryFloatMilliVolt.first > 0) { + output["FloatVoltage"]["v"] = mpptData.BatteryFloatMilliVolt.second / 1000.0; + output["FloatVoltage"]["u"] = "V"; + output["FloatVoltage"]["d"] = "2"; + } + + const JsonObject input = values["input"].to(); + if (mpptData.NetworkTotalDcInputPowerMilliWatts.first > 0) { + input["NetworkPower"]["v"] = mpptData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0; + input["NetworkPower"]["u"] = "W"; + input["NetworkPower"]["d"] = "0"; + } + input["PPV"]["v"] = mpptData.panelPower_PPV_W; + input["PPV"]["u"] = "W"; + input["PPV"]["d"] = 0; + input["VPV"]["v"] = mpptData.panelVoltage_VPV_mV / 1000.0; + input["VPV"]["u"] = "V"; + input["VPV"]["d"] = 2; + input["IPV"]["v"] = mpptData.panelCurrent_mA / 1000.0; + input["IPV"]["u"] = "A"; + input["IPV"]["d"] = 2; + + if (mpptData.yieldToday_H20_Wh >= 1000.0) { + input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh / 1000.0; + input["YieldToday"]["u"] = "kWh"; + input["YieldToday"]["d"] = 2; + } else { + input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh; + input["YieldToday"]["u"] = "Wh"; + input["YieldToday"]["d"] = 0; + } + + if (mpptData.yieldYesterday_H22_Wh >= 1000.0) { + input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh / 1000.0; + input["YieldYesterday"]["u"] = "kWh"; + input["YieldYesterday"]["d"] = 2; + } else { + input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh; + input["YieldYesterday"]["u"] = "Wh"; + input["YieldYesterday"]["d"] = 0; + } + + input["YieldTotal"]["v"] = mpptData.yieldTotal_H19_Wh / 1000.0; + input["YieldTotal"]["u"] = "kWh"; + input["YieldTotal"]["d"] = 2; + input["MaximumPowerToday"]["v"] = mpptData.maxPowerToday_H21_W; + input["MaximumPowerToday"]["u"] = "W"; + input["MaximumPowerToday"]["d"] = 0; + input["MaximumPowerYesterday"]["v"] = mpptData.maxPowerYesterday_H23_W; + input["MaximumPowerYesterday"]["u"] = "W"; + input["MaximumPowerYesterday"]["d"] = 0; +} + +void Stats::mqttPublish() const +{ + if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) { + auto const& config = Configuration.get(); + + // determine if this cycle should publish full values or updates only + if (_nextPublishFull <= _nextPublishUpdatesOnly) { + _PublishFull = true; + } else { + _PublishFull = !config.SolarCharger.PublishUpdatesOnly; + } + + for (auto const& entry : _data) { + auto currentData = entry.second; + if (!currentData) { continue; } + + auto const& previousData = _previousData[entry.first]; + publishMpptData(*currentData, previousData); + + if (!_PublishFull) { + _previousData[entry.first] = *currentData; + } + } + + // now calculate next points of time to publish + _nextPublishUpdatesOnly = millis() + ::SolarChargers::Stats::getMqttFullPublishIntervalMs(); + + if (_PublishFull) { + // when Home Assistant MQTT-Auto-Discovery is active, + // and "enable expiration" is active, all values must be published at + // least once before the announced expiry interval is reached + if ((config.SolarCharger.PublishUpdatesOnly) && (config.Mqtt.Hass.Enabled) && (config.Mqtt.Hass.Expire)) { + _nextPublishFull = millis() + (((config.Mqtt.PublishInterval * 3) - 1) * 1000); + + } else { + // no future publish full needed + _nextPublishFull = UINT32_MAX; + } + } + } +} + +void Stats::publishMpptData(const VeDirectMpptController::data_t ¤tData, const VeDirectMpptController::data_t &previousData) const { + String value; + String topic = "victron/"; + topic.concat(currentData.serialNr_SER); + topic.concat("/"); + +#define PUBLISH(sm, t, val) \ + if (_PublishFull || currentData.sm != previousData.sm) { \ + MqttSettings.publish(topic + t, String(val)); \ + } + + PUBLISH(productID_PID, "PID", currentData.getPidAsString().data()); + PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER); + PUBLISH(firmwareVer_FW, "FWI", currentData.getFwVersionAsInteger()); + PUBLISH(firmwareVer_FW, "FWF", currentData.getFwVersionFormatted()); + PUBLISH(firmwareVer_FW, "FW", currentData.firmwareVer_FW); + PUBLISH(firmwareVer_FWE, "FWE", currentData.firmwareVer_FWE); + PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data()); + PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data()); + PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data()); + PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data()); + PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS); + PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0); + PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0); + PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W); + PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0); + PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0); + PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W); + PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent); + PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0); + PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0); + PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W); + PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0); + PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W); +#undef PUBLILSH + +#define PUBLISH_OPT(sm, t, val) \ + if (currentData.sm.first != 0 && (_PublishFull || currentData.sm.second != previousData.sm.second)) { \ + MqttSettings.publish(topic + t, String(val)); \ + } + + PUBLISH_OPT(relayState_RELAY, "RELAY", currentData.relayState_RELAY.second ? "ON" : "OFF"); + PUBLISH_OPT(loadOutputState_LOAD, "LOAD", currentData.loadOutputState_LOAD.second ? "ON" : "OFF"); + PUBLISH_OPT(loadCurrent_IL_mA, "IL", currentData.loadCurrent_IL_mA.second / 1000.0); + PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); + PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0); + PUBLISH_OPT(BatteryAbsorptionMilliVolt, "BatteryAbsorption", currentData.BatteryAbsorptionMilliVolt.second / 1000.0); + PUBLISH_OPT(BatteryFloatMilliVolt, "BatteryFloat", currentData.BatteryFloatMilliVolt.second / 1000.0); + PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); +#undef PUBLILSH_OPT +} + +void Stats::mqttPublishSensors(const boolean forcePublish) const +{ + // TODO(andreasboehm): sensors are only published once, and then never again. + // This matches the old implementation, but is not ideal. We should publish + // sensors whenever a new controller is discovered, or when the amount of available + // datapoints for a controller changed. + if (!forcePublish) { return; } + + for (auto entry : _data) { + if (!entry.second) { continue; } + _hassIntegration.publishSensors(*entry.second); + } +} + +}; // namespace SolarChargers::Victron diff --git a/webapp/package.json b/webapp/package.json index a646f6c0f..d057a7f15 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -17,35 +17,35 @@ "bootstrap": "^5.3.3", "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", - "sortablejs": "^1.15.3", + "sortablejs": "^1.15.6", "spark-md5": "^3.0.2", - "vue": "^3.5.12", - "vue-i18n": "10.0.4", - "vue-router": "^4.4.5" + "vue": "^3.5.13", + "vue-i18n": "11.0.1", + "vue-router": "^4.5.0" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^5.2.0", + "@intlify/unplugin-vue-i18n": "^6.0.3", "@tsconfig/node22": "^22.0.0", "@types/bootstrap": "^5.2.10", - "@types/node": "^22.9.0", + "@types/node": "^22.10.6", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.5", - "@vitejs/plugin-vue": "^5.1.4", - "@vue/eslint-config-typescript": "^14.1.3", - "@vue/tsconfig": "^0.5.1", - "eslint": "^9.14.0", - "eslint-plugin-vue": "^9.30.0", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-typescript": "^14.3.0", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.18.0", + "eslint-plugin-vue": "^9.32.0", "npm-run-all": "^4.1.5", - "prettier": "^3.3.3", + "prettier": "^3.4.2", "pulltorefreshjs": "^0.1.22", "sass": "=1.77.6", - "terser": "^5.36.0", - "typescript": "^5.6.3", - "vite": "^5.4.10", + "terser": "^5.37.0", + "typescript": "~5.6.3", + "vite": "^6.0.7", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.2", - "vue-tsc": "^2.1.10" + "vue-tsc": "^2.2.0" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue index 78e151280..0b4c0cffa 100644 --- a/webapp/src/components/BatteryView.vue +++ b/webapp/src/components/BatteryView.vue @@ -13,7 +13,7 @@ class="card-header d-flex justify-content-between align-items-center" :class="{ 'text-bg-danger': batteryData.data_age >= 20, - 'text-bg-primary': batteryData.data_age < 20, + 'text-bg-success': batteryData.data_age < 20, }" >
diff --git a/webapp/src/components/DevInfo.vue b/webapp/src/components/DevInfo.vue index a56f8ce64..da2d81aa1 100644 --- a/webapp/src/components/DevInfo.vue +++ b/webapp/src/components/DevInfo.vue @@ -48,8 +48,9 @@ {{ $t('devinfo.SupportsPowerDistributionLogic') }} - {{ $t('devinfo.yes') }} - {{ $t('devinfo.no') }} + + + @@ -58,6 +59,7 @@