diff --git a/include/Configuration.h b/include/Configuration.h index 42d861418..6094052cd 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -10,7 +10,7 @@ #define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change -#define CONFIG_VERSION_ONBATTERY 2 +#define CONFIG_VERSION_ONBATTERY 3 #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -197,6 +197,16 @@ struct BATTERY_CONFIG_T { }; using BatteryConfig = struct BATTERY_CONFIG_T; +enum SolarChargerProviderType { VEDIRECT = 0 }; + +struct SOLAR_CHARGER_CONFIG_T { + bool Enabled; + bool VerboseLogging; + SolarChargerProviderType Provider; + bool PublishUpdatesOnly; +}; +using SolarChargerConfig = struct SOLAR_CHARGER_CONFIG_T; + struct CONFIG_T { struct { uint32_t Version; @@ -308,11 +318,7 @@ struct CONFIG_T { uint8_t Brightness; } Led_Single[PINMAPPING_LED_COUNT]; - struct { - bool Enabled; - bool VerboseLogging; - bool UpdatesOnly; - } Vedirect; + SolarChargerConfig SolarCharger; struct PowerMeterConfig { bool Enabled; @@ -374,6 +380,7 @@ class ConfigurationClass { void deleteInverterById(const uint8_t id); static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); + static void serializeSolarChargerConfig(SolarChargerConfig 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); @@ -382,6 +389,7 @@ class ConfigurationClass { static void serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target); static void deserializeHttpRequestConfig(JsonObject const& source_http_config, HttpRequestConfig& target); + static void deserializeSolarChargerConfig(JsonObject const& source, SolarChargerConfig& 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 016ee804a..000000000 --- a/include/MqttHandleVedirect.h +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "VeDirectMpptController.h" -#include "Configuration.h" -#include -#include -#include - -#ifndef VICTRON_PIN_RX -#define VICTRON_PIN_RX 22 -#endif - -#ifndef VICTRON_PIN_TX -#define VICTRON_PIN_TX 21 -#endif - -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/VictronMppt.h b/include/VictronMppt.h deleted file mode 100644 index e79564e87..000000000 --- a/include/VictronMppt.h +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include - -#include "VeDirectMpptController.h" -#include "Configuration.h" -#include - -class VictronMpptClass { -public: - VictronMpptClass() = default; - ~VictronMpptClass() = default; - - void init(Scheduler& scheduler); - void updateSettings(); - - bool isDataValid() const; - bool isDataValid(size_t idx) const; - - // returns the data age of all controllers, - // i.e, the youngest data's age is returned. - uint32_t getDataAgeMillis() const; - uint32_t getDataAgeMillis(size_t idx) const; - - size_t controllerAmount() const { return _controllers.size(); } - std::optional getData(size_t idx = 0) const; - - // total output of all MPPT charge controllers in Watts - int32_t getPowerOutputWatts() 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; - - // 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: - void loop(); - VictronMpptClass(VictronMpptClass const& other) = delete; - VictronMpptClass(VictronMpptClass&& other) = delete; - VictronMpptClass& operator=(VictronMpptClass const& other) = delete; - VictronMpptClass& operator=(VictronMpptClass&& other) = delete; - - Task _loopTask; - - 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); -}; - -extern VictronMpptClass VictronMppt; diff --git a/include/WebApi.h b/include/WebApi.h index 07ffaa22b..c6da12d93 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -27,8 +27,8 @@ #include "WebApi_ws_console.h" #include "WebApi_ws_live.h" #include -#include "WebApi_ws_vedirect_live.h" -#include "WebApi_vedirect.h" +#include "WebApi_ws_solarcharger_live.h" +#include "WebApi_solarcharger.h" #include "WebApi_ws_Huawei.h" #include "WebApi_Huawei.h" #include "WebApi_ws_battery.h" @@ -79,8 +79,8 @@ class WebApiClass { WebApiWebappClass _webApiWebapp; WebApiWsConsoleClass _webApiWsConsole; WebApiWsLiveClass _webApiWsLive; - WebApiWsVedirectLiveClass _webApiWsVedirectLive; - WebApiVedirectClass _webApiVedirect; + WebApiWsSolarChargerLiveClass _webApiWsSolarChargerLive; + WebApiSolarChargerlass _webApiSolarCharger; WebApiHuaweiClass _webApiHuaweiClass; WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; WebApiWsBatteryLiveClass _webApiWsBatteryLive; diff --git a/include/WebApi_vedirect.h b/include/WebApi_solarcharger.h similarity index 53% rename from include/WebApi_vedirect.h rename to include/WebApi_solarcharger.h index dd4ec90ac..38d155683 100644 --- a/include/WebApi_vedirect.h +++ b/include/WebApi_solarcharger.h @@ -5,14 +5,14 @@ #include -class WebApiVedirectClass { +class WebApiSolarChargerlass { public: void init(AsyncWebServer& server, Scheduler& scheduler); private: - void onVedirectStatus(AsyncWebServerRequest* request); - void onVedirectAdminGet(AsyncWebServerRequest* request); - void onVedirectAdminPost(AsyncWebServerRequest* request); + void onStatus(AsyncWebServerRequest* request); + void onAdminGet(AsyncWebServerRequest* request); + void onAdminPost(AsyncWebServerRequest* request); AsyncWebServer* _server; -}; \ No newline at end of file +}; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index e02f9a8c1..88654edd9 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -31,7 +31,7 @@ class WebApiWsLiveClass { AuthenticationMiddleware _simpleDigestAuth; uint32_t _lastPublishOnBatteryFull = 0; - uint32_t _lastPublishVictron = 0; + uint32_t _lastPublishSolarCharger = 0; uint32_t _lastPublishHuawei = 0; uint32_t _lastPublishBattery = 0; uint32_t _lastPublishPowerMeter = 0; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_solarcharger_live.h similarity index 79% rename from include/WebApi_ws_vedirect_live.h rename to include/WebApi_ws_solarcharger_live.h index 7c3bedf60..b2ee01f1a 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_solarcharger_live.h @@ -8,18 +8,16 @@ #include #include -class WebApiWsVedirectLiveClass { +class WebApiWsSolarChargerLiveClass { public: - WebApiWsVedirectLiveClass(); + WebApiWsSolarChargerLiveClass(); void init(AsyncWebServer& server, Scheduler& scheduler); void reload(); 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; @@ -27,7 +25,6 @@ class WebApiWsVedirectLiveClass { uint32_t _lastFullPublish = 0; uint32_t _lastPublish = 0; - uint16_t responseSize() const; std::mutex _mutex; diff --git a/include/defaults.h b/include/defaults.h index 7d554e2e9..35169d42c 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -115,9 +115,9 @@ #define LANG_PACK_SUFFIX ".lang.json" // values specific to downstream project OpenDTU-OnBattery start here: -#define VEDIRECT_ENABLED false -#define VEDIRECT_VERBOSE_LOGGING false -#define VEDIRECT_UPDATESONLY true +#define SOLAR_CHARGER_ENABLED false +#define SOLAR_CHARGER_VERBOSE_LOGGING false +#define SOLAR_CHARGER_PUBLISH_UPDATES_ONLY true #define POWERMETER_ENABLED false #define POWERMETER_POLLING_INTERVAL 10 diff --git a/include/solarcharger/Controller.h b/include/solarcharger/Controller.h new file mode 100644 index 000000000..ec4645d43 --- /dev/null +++ b/include/solarcharger/Controller.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#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 _publishSensors = 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..466b53405 --- /dev/null +++ b/include/solarcharger/DummyStats.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace SolarChargers { + +class DummyStats : public Stats { +public: + uint32_t getAgeMillis() const override { return 0; } + std::optional getOutputPowerWatts() const override { return std::nullopt; } + std::optional getOutputVoltage() const override { return std::nullopt; } + int32_t getPanelPowerWatts() const override { return 0; } + float getYieldTotal() const override { return 0; } + float getYieldDay() const override { return 0; } + void getLiveViewData(JsonVariant& root, boolean fullUpdate, uint32_t lastPublish) const override {} + void mqttPublish() const override {} +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/HassIntegration.h b/include/solarcharger/HassIntegration.h new file mode 100644 index 000000000..54662b11c --- /dev/null +++ b/include/solarcharger/HassIntegration.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +namespace SolarChargers { + +class HassIntegration { +public: + virtual void publishSensors() const; + +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..a879c3be6 --- /dev/null +++ b/include/solarcharger/Provider.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +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; + virtual HassIntegration const& getHassIntegration() const = 0; +}; + +} // namespace SolarChargers diff --git a/include/solarcharger/Stats.h b/include/solarcharger/Stats.h new file mode 100644 index 000000000..1d9e19fd9 --- /dev/null +++ b/include/solarcharger/Stats.h @@ -0,0 +1,45 @@ +// 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 int32_t getPanelPowerWatts() const; + + // sum of total yield of all MPPT charge controllers in kWh + virtual float getYieldTotal() const; + + // sum of today's yield of all MPPT charge controllers in Wh + virtual float getYieldDay() const; + + // convert stats to JSON for web application live view + virtual void getLiveViewData(JsonVariant& root, boolean fullUpdate, uint32_t lastPublish) const; + + void mqttLoop(); + + // 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/MqttHandleVedirectHass.h b/include/solarcharger/victron/HassIntegration.h similarity index 64% rename from include/MqttHandleVedirectHass.h rename to include/solarcharger/victron/HassIntegration.h index 6d7a17ac6..dbe133ebd 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/solarcharger/victron/HassIntegration.h @@ -2,32 +2,26 @@ #pragma once #include +#include #include "VeDirectMpptController.h" -#include -class MqttHandleVedirectHassClass { +namespace SolarChargers::Victron { + +class HassIntegration : public ::SolarChargers::HassIntegration { public: - void init(Scheduler& scheduler); - void publishConfig(); - void forceUpdate(); + void publishSensors() const final; private: - void loop(); - 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); + const VeDirectMpptController::data_t &mpptData) const; void createDeviceInfo(JsonObject &object, - const VeDirectMpptController::data_t &mpptData); - - Task _loopTask; + const VeDirectMpptController::data_t &mpptData) const; - bool _wasConnected = false; - bool _updateForced = false; }; -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..c3711a6af --- /dev/null +++ b/include/solarcharger/victron/Provider.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#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; } + ::SolarChargers::HassIntegration const& getHassIntegration() const final { return _hassIntegration; } + +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(); + HassIntegration _hassIntegration; + + 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..fc3c797eb --- /dev/null +++ b/include/solarcharger/victron/Stats.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#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; + int32_t getPanelPowerWatts() const final; + float getYieldTotal() const final; + float getYieldDay() const final; + + void getLiveViewData(JsonVariant& root, boolean fullUpdate, uint32_t lastPublish) const final; + void mqttPublish() 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; + + void populateJsonWithInstanceStats(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) const; + + void publish_mppt_data(const VeDirectMpptController::data_t &mpptData, const VeDirectMpptController::data_t &frame) const; +}; + +} // namespace SolarChargers::Victron 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/src/Configuration.cpp b/src/Configuration.cpp index cf3bafd78..b03c0c01d 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -38,6 +38,14 @@ void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& sou target_http_config["timeout"] = source.Timeout; } +void ConfigurationClass::serializeSolarChargerConfig(SolarChargerConfig const& source, JsonObject& target) +{ + target["enabled"] = source.Enabled; + target["verbose_logging"] = source.VerboseLogging; + target["provider"] = source.Provider; + target["publish_updates_only"] = source.PublishUpdatesOnly; +} + void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target) { JsonArray values = target["values"].to(); @@ -298,10 +306,8 @@ bool ConfigurationClass::write() } } - JsonObject vedirect = doc["vedirect"].to(); - vedirect["enabled"] = config.Vedirect.Enabled; - vedirect["verbose_logging"] = config.Vedirect.VerboseLogging; - vedirect["updates_only"] = config.Vedirect.UpdatesOnly; + JsonObject solarcharger = doc["solarcharger"].to(); + serializeSolarChargerConfig(config.SolarCharger, solarcharger); JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; @@ -365,6 +371,14 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source_h target.Timeout = source_http_config["timeout"] | HTTP_REQUEST_TIMEOUT_MS; } +void ConfigurationClass::deserializeSolarChargerConfig(JsonObject const& source, SolarChargerConfig& target) +{ + target.Enabled = source["enabled"] | SOLAR_CHARGER_ENABLED; + target.VerboseLogging = source["verbose_logging"] | VERBOSE_LOGGING; + target.Provider = source["provider"] | SolarChargerProviderType::VEDIRECT; + target.PublishUpdatesOnly = source["publish_updates_only"] | SOLAR_CHARGER_PUBLISH_UPDATES_ONLY; +} + void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) { for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { @@ -656,10 +670,7 @@ bool ConfigurationClass::read() } } - JsonObject vedirect = doc["vedirect"]; - config.Vedirect.Enabled = vedirect["enabled"] | VEDIRECT_ENABLED; - config.Vedirect.VerboseLogging = vedirect["verbose_logging"] | VEDIRECT_VERBOSE_LOGGING; - config.Vedirect.UpdatesOnly = vedirect["updates_only"] | VEDIRECT_UPDATESONLY; + deserializeSolarChargerConfig(doc["solarcharger"], config.SolarCharger); JsonObject powermeter = doc["powermeter"]; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; @@ -901,6 +912,13 @@ void ConfigurationClass::migrateOnBattery() config.PowerLimiter.ConductionLosses = doc["powerlimiter"]["solar_passthrough_losses"].as(); } + if (config.Cfg.VersionOnBattery < 3) { + JsonObject vedirect = doc["vedirect"]; + config.SolarCharger.Enabled = vedirect["enabled"] | SOLAR_CHARGER_ENABLED; + config.SolarCharger.VerboseLogging = vedirect["verbose_logging"] | SOLAR_CHARGER_VERBOSE_LOGGING; + config.SolarCharger.PublishUpdatesOnly = vedirect["updates_only"] | SOLAR_CHARGER_PUBLISH_UPDATES_ONLY; + } + f.close(); config.Cfg.VersionOnBattery = CONFIG_VERSION_ONBATTERY; diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index d4861e629..74b9874f6 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -100,7 +100,7 @@ void MqttHandlePowerLimiterClass::loop() MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); - if (config.Vedirect.Enabled) { + if (config.SolarCharger.Enabled) { MqttSettings.publish("powerlimiter/status/full_solar_passthrough_active", String(PowerLimiter.getFullSolarPassThroughEnabled())); MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage)); MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage)); @@ -111,7 +111,7 @@ void MqttHandlePowerLimiterClass::loop() MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold)); MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold)); - if (config.Vedirect.Enabled) { + if (config.SolarCharger.Enabled) { MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc)); } } diff --git a/src/MqttHandlePowerLimiterHass.cpp b/src/MqttHandlePowerLimiterHass.cpp index 43642f9fb..7187df946 100644 --- a/src/MqttHandlePowerLimiterHass.cpp +++ b/src/MqttHandlePowerLimiterHass.cpp @@ -75,7 +75,7 @@ void MqttHandlePowerLimiterHassClass::publishConfig() publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging", "config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60, 0.1); - if (config.Vedirect.Enabled) { + if (config.SolarCharger.Enabled) { publishBinarySensor("full solar passthrough active", "mdi:transmission-tower-import", "full_solar_passthrough_active", "1", "0"); @@ -96,7 +96,7 @@ void MqttHandlePowerLimiterHassClass::publishConfig() publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging", "config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100, 1.0); - if (config.Vedirect.Enabled) { + if (config.SolarCharger.Enabled) { publishNumber("DPL full solar passthrough SoC", "mdi:transmission-tower-import", "config", "threshold/soc/full_solar_passthrough", diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp deleted file mode 100644 index 55334df50..000000000 --- a/src/MqttHandleVedirect.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Helge Erbe and others - */ -#include "VictronMppt.h" -#include "MqttHandleVedirect.h" -#include "MqttSettings.h" -#include "MessageOutput.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.Vedirect.Enabled) { - 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.Vedirect.UpdatesOnly; - } - - #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 < VictronMppt.controllerAmount(); ++idx) { - std::optional optMpptData = VictronMppt.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.Vedirect.UpdatesOnly) && (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/PowerLimiter.cpp b/src/PowerLimiter.cpp index e3e7e461a..2596ad03a 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -10,13 +10,13 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "Huawei_can.h" -#include #include "MessageOutput.h" #include #include #include #include #include "SunPosition.h" +#include static auto sBatteryPoweredFilter = [](PowerLimiterInverter const& inv) { return !inv.isSolarPowered(); @@ -370,8 +370,11 @@ float PowerLimiterClass::getBatteryVoltage(bool log) { if (inverter.first > 0) { res = inverter.first; } float chargeControllerVoltage = -1; - if (VictronMppt.isDataValid()) { - res = chargeControllerVoltage = static_cast(VictronMppt.getOutputVoltage()); + + auto chargerOutputVoltage = SolarCharger.getStats()->getOutputVoltage(); + + if (chargerOutputVoltage) { + res = chargeControllerVoltage = *chargerOutputVoltage; } float bmsVoltage = -1; @@ -425,8 +428,9 @@ void PowerLimiterClass::fullSolarPassthrough(PowerLimiterClass::Status reason) uint16_t targetOutput = 0; - if (VictronMppt.isDataValid()) { - targetOutput = static_cast(std::max(0, VictronMppt.getPowerOutputWatts())); + auto solarChargerOuput = SolarCharger.getStats()->getOutputPowerWatts(); + if (solarChargerOuput) { + targetOutput = static_cast(std::max(0, *solarChargerOuput)); targetOutput = dcPowerBusToInverterAc(targetOutput); } @@ -675,14 +679,16 @@ bool PowerLimiterClass::updateInverters() uint16_t PowerLimiterClass::getSolarPassthroughPower() { auto const& config = Configuration.get(); + auto solarChargerOutput = SolarCharger.getStats()->getOutputPowerWatts(); if (!config.PowerLimiter.SolarPassThroughEnabled || isBelowStopThreshold() - || !VictronMppt.isDataValid()) { + || !solarChargerOutput + ) { return 0; } - return VictronMppt.getPowerOutputWatts(); + return *solarChargerOutput; } float PowerLimiterClass::getBatteryInvertersOutputAcWatts() diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp deleted file mode 100644 index 86373f453..000000000 --- a/src/VictronMppt.cpp +++ /dev/null @@ -1,264 +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" - -VictronMpptClass VictronMppt; - -void VictronMpptClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback([this] { loop(); }); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - this->updateSettings(); -} - -void VictronMpptClass::updateSettings() -{ - std::lock_guard lock(_mutex); - - _controllers.clear(); - for (auto const& o: _serialPortOwners) { - SerialPortManager.freePort(o.c_str()); - } - _serialPortOwners.clear(); - - auto const& config = Configuration.get(); - if (!config.Vedirect.Enabled) { return; } - - const PinMapping_t& pin = PinMapping.get(); - - initController(pin.victron_rx, pin.victron_tx, - config.Vedirect.VerboseLogging, 1); - - initController(pin.victron_rx2, pin.victron_tx2, - config.Vedirect.VerboseLogging, 2); - - initController(pin.victron_rx3, pin.victron_tx3, - config.Vedirect.VerboseLogging, 3); -} - -bool VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::isDataValid() const -{ - std::lock_guard lock(_mutex); - - for (auto const& upController: _controllers) { - if (upController->isDataValid()) { return true; } - } - - return false; -} - -bool VictronMpptClass::isDataValid(size_t idx) const -{ - std::lock_guard lock(_mutex); - - if (_controllers.empty() || idx >= _controllers.size()) { - return false; - } - - return _controllers[idx]->isDataValid(); -} - -uint32_t VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::getPowerOutputWatts() 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 VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::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 VictronMpptClass::getStateOfOperation() const -{ - for (const auto& upController : _controllers) { - if (upController->isDataValid()) { - return upController->getData().currentState_CS; - } - } - - return std::nullopt; -} - -std::optional VictronMpptClass::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/WebApi.cpp b/src/WebApi.cpp index 08026c4ec..9e869a52e 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -39,8 +39,8 @@ void WebApiClass::init(Scheduler& scheduler) _webApiBattery.init(_server, scheduler); _webApiPowerMeter.init(_server, scheduler); _webApiPowerLimiter.init(_server, scheduler); - _webApiWsVedirectLive.init(_server, scheduler); - _webApiVedirect.init(_server, scheduler); + _webApiWsSolarChargerLive.init(_server, scheduler); + _webApiSolarCharger.init(_server, scheduler); _webApiWsHuaweiLive.init(_server, scheduler); _webApiHuaweiClass.init(_server, scheduler); _webApiWsBatteryLive.init(_server, scheduler); @@ -53,7 +53,7 @@ void WebApiClass::reload() _webApiWsConsole.reload(); _webApiWsLive.reload(); _webApiWsBatteryLive.reload(); - _webApiWsVedirectLive.reload(); + _webApiWsSolarChargerLive.reload(); _webApiWsHuaweiLive.reload(); } diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 1d0c2ab15..79008fc54 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) { @@ -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_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 196bc5be0..30e8f5b3a 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -3,7 +3,6 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_powerlimiter.h" -#include "VeDirectFrameHandler.h" #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" @@ -50,7 +49,7 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) root["power_meter_enabled"] = config.PowerMeter.Enabled; root["battery_enabled"] = config.Battery.Enabled; - root["charge_controller_enabled"] = config.Vedirect.Enabled; + root["charge_controller_enabled"] = config.SolarCharger.Enabled; JsonArray inverters = root["inverters"].to(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 911bcd398..35fc04dcd 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -3,11 +3,9 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_powermeter.h" -#include "VeDirectFrameHandler.h" #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_solar_charger.cpp new file mode 100644 index 000000000..454d0f45a --- /dev/null +++ b/src/WebApi_solar_charger.cpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "WebApi_solarcharger.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include "helper.h" +#include "MqttHandlePowerLimiterHass.h" +#include + +void WebApiSolarChargerlass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + _server = &server; + + _server->on("/api/solarcharger/config", HTTP_GET, std::bind(&WebApiSolarChargerlass::onAdminGet, this, _1)); + _server->on("/api/solarcharger/config", HTTP_POST, std::bind(&WebApiSolarChargerlass::onAdminPost, this, _1)); +} + +void WebApiSolarChargerlass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto root = response->getRoot().as(); + auto const& config = Configuration.get(); + + ConfigurationClass::serializeSolarChargerConfig(config.SolarCharger, root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiSolarChargerlass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } + + auto& retMsg = response->getRoot(); + + if (!root["enabled"].is() || + !root["provider"].is() || + !root["verbose_logging"].is() || + !root["publish_updates_only"].is()) { + retMsg["message"] = "Values are missing!"; + retMsg["code"] = WebApiError::GenericValueMissing; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } + + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + ConfigurationClass::deserializeSolarChargerConfig(root.as(), config.SolarCharger); + } + + WebApi.writeConfig(retMsg); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + SolarCharger.updateSettings(); + + // potentially make solar passthrough thresholds auto-discoverable + MqttHandlePowerLimiterHass.forceUpdate(); +} diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp deleted file mode 100644 index 06493ab88..000000000 --- a/src/WebApi_vedirect.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "WebApi_vedirect.h" -#include "VictronMppt.h" -#include "ArduinoJson.h" -#include "AsyncJson.h" -#include "Configuration.h" -#include "WebApi.h" -#include "WebApi_errors.h" -#include "helper.h" -#include "MqttHandlePowerLimiterHass.h" - -void WebApiVedirectClass::init(AsyncWebServer& server, Scheduler& scheduler) -{ - using std::placeholders::_1; - - _server = &server; - - _server->on("/api/vedirect/status", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectStatus, this, _1)); - _server->on("/api/vedirect/config", HTTP_GET, std::bind(&WebApiVedirectClass::onVedirectAdminGet, this, _1)); - _server->on("/api/vedirect/config", HTTP_POST, std::bind(&WebApiVedirectClass::onVedirectAdminPost, this, _1)); -} - -void WebApiVedirectClass::onVedirectStatus(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - auto const& config = Configuration.get(); - - root["vedirect_enabled"] = config.Vedirect.Enabled; - root["verbose_logging"] = config.Vedirect.VerboseLogging; - root["vedirect_updatesonly"] = config.Vedirect.UpdatesOnly; - - response->setLength(); - request->send(response); -} - -void WebApiVedirectClass::onVedirectAdminGet(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - auto const& config = Configuration.get(); - - root["vedirect_enabled"] = config.Vedirect.Enabled; - root["verbose_logging"] = config.Vedirect.VerboseLogging; - root["vedirect_updatesonly"] = config.Vedirect.UpdatesOnly; - - response->setLength(); - request->send(response); -} - -void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonDocument root; - if (!WebApi.parseRequestData(request, response, root)) { - return; - } - - auto& retMsg = response->getRoot(); - - if (!root["vedirect_enabled"].is() || - !root["verbose_logging"].is() || - !root["vedirect_updatesonly"].is() ) { - retMsg["message"] = "Values are missing!"; - retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); - return; - } - - { - auto guard = Configuration.getWriteGuard(); - auto& config = guard.getConfig(); - config.Vedirect.Enabled = root["vedirect_enabled"].as(); - config.Vedirect.VerboseLogging = root["verbose_logging"].as(); - config.Vedirect.UpdatesOnly = root["vedirect_updatesonly"].as(); - } - - WebApi.writeConfig(retMsg); - - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - - - VictronMppt.updateSettings(); - - // potentially make solar passthrough thresholds auto-discoverable - MqttHandlePowerLimiterHass.forceUpdate(); -} diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 36031a357..ea2fbf373 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -10,8 +10,8 @@ #include "Battery.h" #include "Huawei_can.h" #include "PowerMeter.h" -#include "VictronMppt.h" #include "defaults.h" +#include #include WebApiWsLiveClass::WebApiWsLiveClass() @@ -72,19 +72,28 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al auto const& config = Configuration.get(); auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; - auto victronAge = VictronMppt.getDataAgeMillis(); - if (all || (victronAge > 0 && (millis() - _lastPublishVictron) > victronAge)) { - auto vedirectObj = root["vedirect"].to(); - vedirectObj["enabled"] = config.Vedirect.Enabled; + auto solarChargerAge = SolarCharger.getStats()->getAgeMillis(); + if (all || (solarChargerAge > 0 && (millis() - _lastPublishSolarCharger) > solarChargerAge)) { + auto solarchargerObj = root["solarcharger"].to(); + solarchargerObj["enabled"] = config.SolarCharger.Enabled; - if (config.Vedirect.Enabled) { - auto totalVeObj = vedirectObj["total"].to(); - addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); - addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + if (config.SolarCharger.Enabled) { + auto totalVeObj = solarchargerObj["total"].to(); + + auto power = SolarCharger.getStats()->getPanelPowerWatts(); + auto outputPower = SolarCharger.getStats()->getOutputPowerWatts(); + + // use output power if available, because it is more accurate + if (outputPower) { + power = *outputPower; + } + + addTotalField(totalVeObj, "Power", power, "W", 1); + addTotalField(totalVeObj, "YieldDay", SolarCharger.getStats()->getYieldDay(), "Wh", 0); + addTotalField(totalVeObj, "YieldTotal", SolarCharger.getStats()->getYieldTotal(), "kWh", 2); } - if (!all) { _lastPublishVictron = millis(); } + if (!all) { _lastPublishSolarCharger = millis(); } } if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) { @@ -370,7 +379,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateOnBatteryJsonResponse(root, true); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - + } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); WebApi.sendTooManyRequests(request); diff --git a/src/WebApi_ws_solarcharger_live.cpp b/src/WebApi_ws_solarcharger_live.cpp new file mode 100644 index 000000000..533877c4c --- /dev/null +++ b/src/WebApi_ws_solarcharger_live.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_ws_solarcharger_live.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "Utils.h" +#include "WebApi.h" +#include "defaults.h" +#include "PowerLimiter.h" +#include + +WebApiWsSolarChargerLiveClass::WebApiWsSolarChargerLiveClass() + : _ws("/solarchargerlivedata") +{ +} + +void WebApiWsSolarChargerLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + _server = &server; + _server->on("/api/solarchargerlivedata/status", HTTP_GET, std::bind(&WebApiWsSolarChargerLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsSolarChargerLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); + + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.setCallback(std::bind(&WebApiWsSolarChargerLiveClass::wsCleanupTaskCb, this)); + _wsCleanupTask.setIterations(TASK_FOREVER); + _wsCleanupTask.setInterval(1 * TASK_SECOND); + _wsCleanupTask.enable(); + + scheduler.addTask(_sendDataTask); + _sendDataTask.setCallback(std::bind(&WebApiWsSolarChargerLiveClass::sendDataTaskCb, this)); + _sendDataTask.setIterations(TASK_FOREVER); + _sendDataTask.setInterval(500 * TASK_MILLISECOND); + _sendDataTask.enable(); + + _simpleDigestAuth.setUsername(AUTH_USERNAME); + _simpleDigestAuth.setRealm("solarcharger websocket"); + + reload(); +} + +void WebApiWsSolarChargerLiveClass::reload() +{ + _ws.removeMiddleware(&_simpleDigestAuth); + + auto const& config = Configuration.get(); + + if (config.Security.AllowReadonly) { return; } + + _ws.enable(false); + _simpleDigestAuth.setPassword(config.Security.Password); + _ws.addMiddleware(&_simpleDigestAuth); + _ws.closeAll(); + _ws.enable(true); +} + +void WebApiWsSolarChargerLiveClass::wsCleanupTaskCb() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + _ws.cleanupClients(); +} + +void WebApiWsSolarChargerLiveClass::sendDataTaskCb() +{ + // do nothing if no WS client is connected + if (_ws.count() == 0) { return; } + + // Update on ve.direct change or at least after 10 seconds + bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000)); + + auto publishAgeMillis = millis() - _lastPublish; + bool updateAvailable = SolarCharger.getStats()->getAgeMillis() < publishAgeMillis; + + if (fullUpdate || updateAvailable) { + try { + std::lock_guard lock(_mutex); + JsonDocument root; + JsonVariant var = root; + + generateCommonJsonResponse(var, fullUpdate); + + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer);; + } + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Calling /api/solarchargerlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/solarchargerlivedata/status. Reason: \"%s\".\r\n", exc.what()); + } + } + + if (fullUpdate) { + _lastFullPublish = millis(); + } +} + +void WebApiWsSolarChargerLiveClass::generateCommonJsonResponse(JsonVariant& root, bool fullUpdate) +{ + SolarCharger.getStats()->getLiveViewData(root, fullUpdate, _lastPublish); + _lastPublish = millis(); +} + +void WebApiWsSolarChargerLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } else if (type == WS_EVT_DISCONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } +} + +void WebApiWsSolarChargerLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + try { + std::lock_guard lock(_mutex); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + + generateCommonJsonResponse(root, true/*fullUpdate*/); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Calling /api/solarchargerlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + WebApi.sendTooManyRequests(request); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/solarchargerlivedata/status. Reason: \"%s\".\r\n", exc.what()); + WebApi.sendTooManyRequests(request); + } +} diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp deleted file mode 100644 index 5189d0b3a..000000000 --- a/src/WebApi_ws_vedirect_live.cpp +++ /dev/null @@ -1,292 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "WebApi_ws_vedirect_live.h" -#include "AsyncJson.h" -#include "Configuration.h" -#include "MessageOutput.h" -#include "Utils.h" -#include "WebApi.h" -#include "defaults.h" -#include "PowerLimiter.h" -#include "VictronMppt.h" - -WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass() - : _ws("/vedirectlivedata") -{ -} - -void WebApiWsVedirectLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) -{ - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - _server = &server; - _server->on("/api/vedirectlivedata/status", HTTP_GET, std::bind(&WebApiWsVedirectLiveClass::onLivedataStatus, this, _1)); - - _server->addHandler(&_ws); - _ws.onEvent(std::bind(&WebApiWsVedirectLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - - - scheduler.addTask(_wsCleanupTask); - _wsCleanupTask.setCallback(std::bind(&WebApiWsVedirectLiveClass::wsCleanupTaskCb, this)); - _wsCleanupTask.setIterations(TASK_FOREVER); - _wsCleanupTask.setInterval(1 * TASK_SECOND); - _wsCleanupTask.enable(); - - scheduler.addTask(_sendDataTask); - _sendDataTask.setCallback(std::bind(&WebApiWsVedirectLiveClass::sendDataTaskCb, this)); - _sendDataTask.setIterations(TASK_FOREVER); - _sendDataTask.setInterval(500 * TASK_MILLISECOND); - _sendDataTask.enable(); - - _simpleDigestAuth.setUsername(AUTH_USERNAME); - _simpleDigestAuth.setRealm("vedirect websocket"); - - reload(); -} - -void WebApiWsVedirectLiveClass::reload() -{ - _ws.removeMiddleware(&_simpleDigestAuth); - - auto const& config = Configuration.get(); - - if (config.Security.AllowReadonly) { return; } - - _ws.enable(false); - _simpleDigestAuth.setPassword(config.Security.Password); - _ws.addMiddleware(&_simpleDigestAuth); - _ws.closeAll(); - _ws.enable(true); -} - -void WebApiWsVedirectLiveClass::wsCleanupTaskCb() -{ - // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients - _ws.cleanupClients(); -} - -bool WebApiWsVedirectLiveClass::hasUpdate(size_t idx) -{ - auto dataAgeMillis = VictronMppt.getDataAgeMillis(idx); - if (dataAgeMillis == 0) { return false; } - auto publishAgeMillis = millis() - _lastPublish; - return dataAgeMillis < publishAgeMillis; -} - -uint16_t WebApiWsVedirectLiveClass::responseSize() const -{ - // estimated with ArduinoJson assistant - return VictronMppt.controllerAmount() * (1024 + 512) + 128/*DPL status and structure*/; -} - -void WebApiWsVedirectLiveClass::sendDataTaskCb() -{ - // do nothing if no WS client is connected - if (_ws.count() == 0) { return; } - - // 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 < VictronMppt.controllerAmount(); ++idx) { - if (hasUpdate(idx)) { - updateAvailable = true; - break; - } - } - } - - if (fullUpdate || updateAvailable) { - try { - std::lock_guard lock(_mutex); - JsonDocument root; - JsonVariant var = root; - - generateCommonJsonResponse(var, fullUpdate); - - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - String buffer; - serializeJson(root, buffer); - - _ws.textAll(buffer);; - } - } catch (std::bad_alloc& bad_alloc) { - MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); - } - } - - if (fullUpdate) { - _lastFullPublish = millis(); - } -} - -void WebApiWsVedirectLiveClass::generateCommonJsonResponse(JsonVariant& root, bool fullUpdate) -{ - auto array = root["vedirect"]["instances"].to(); - root["vedirect"]["full_update"] = fullUpdate; - - for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - auto optMpptData = VictronMppt.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"] = VictronMppt.getDataAgeMillis(idx); - populateJson(nested, *optMpptData); - } - - _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 WebApiWsVedirectLiveClass::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 WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) -{ - if (type == WS_EVT_CONNECT) { - char str[64]; - snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); - Serial.println(str); - MessageOutput.println(str); - } else if (type == WS_EVT_DISCONNECT) { - char str[64]; - snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); - Serial.println(str); - MessageOutput.println(str); - } -} - -void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - try { - std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - - generateCommonJsonResponse(root, true/*fullUpdate*/); - - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - } catch (std::bad_alloc& bad_alloc) { - MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); - WebApi.sendTooManyRequests(request); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); - WebApi.sendTooManyRequests(request); - } -} diff --git a/src/main.cpp b/src/main.cpp index 9a4ac2506..c4c438415 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,16 +10,13 @@ #include "Led_Single.h" #include "MessageOutput.h" #include "SerialPortManager.h" -#include "VictronMppt.h" #include "Battery.h" #include "Huawei_can.h" #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" @@ -35,6 +32,7 @@ #include "PowerMeter.h" #include "PowerLimiter.h" #include "defaults.h" +#include #include #include #include @@ -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); @@ -178,7 +174,7 @@ void setup() Datastore.init(scheduler); RestartHelper.init(scheduler); - VictronMppt.init(scheduler); + SolarCharger.init(scheduler); // Power meter PowerMeter.init(scheduler); diff --git a/src/solarcharger/Controller.cpp b/src/solarcharger/Controller.cpp new file mode 100644 index 000000000..1b6320067 --- /dev/null +++ b/src/solarcharger/Controller.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#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; + default: + MessageOutput.printf("[SolarCharger] Unknown provider: %d\r\n", config.SolarCharger.Provider); + return; + } + + if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } + + _publishSensors = 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(); + + _upProvider->getStats()->mqttLoop(); + + auto const& config = Configuration.get(); + if (!config.Mqtt.Hass.Enabled) { return; } + + // TODO(schlimmchen): this cannot make sure that transient + // connection problems are actually always noticed. + if (!MqttSettings.getConnected()) { + _publishSensors = true; + return; + } + + if (!_publishSensors) { return; } + + _upProvider->getHassIntegration().publishSensors(); + + _publishSensors = 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/MqttHandleVedirectHass.cpp b/src/solarcharger/victron/HassIntegration.cpp similarity index 80% rename from src/MqttHandleVedirectHass.cpp rename to src/solarcharger/victron/HassIntegration.cpp index 279c258d7..8af616899 100644 --- a/src/MqttHandleVedirectHass.cpp +++ b/src/solarcharger/victron/HassIntegration.cpp @@ -2,65 +2,23 @@ /* * 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 "VictronMppt.h" #include "Utils.h" #include "__compiled_constants.h" +#include +#include -MqttHandleVedirectHassClass MqttHandleVedirectHass; +namespace SolarChargers::Victron { -void MqttHandleVedirectHassClass::init(Scheduler& scheduler) +void HassIntegration::publishSensors() const { - scheduler.addTask(_loopTask); - _loopTask.setCallback([this] { loop(); }); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); -} - -void MqttHandleVedirectHassClass::loop() -{ - if (!Configuration.get().Vedirect.Enabled) { - 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 ((!Configuration.get().Mqtt.Hass.Enabled) || - (!Configuration.get().Vedirect.Enabled)) { - return; - } - - if (!MqttSettings.getConnected()) { - return; - } - // device info - for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - auto optMpptData = VictronMppt.getData(idx); + for (int idx = 0; idx < 0; ++idx) { + std::optional optMpptData = std::nullopt;// TODO(andreasboehm): How can i get the data in a nice way? .getData(idx); if (!optMpptData.has_value()) { continue; } publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData); @@ -118,14 +76,12 @@ void MqttHandleVedirectHassClass::publishConfig() 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, +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 VeDirectMpptController::data_t &mpptData) const { String serial = mpptData.serialNr_SER; @@ -181,9 +137,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, +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 VeDirectMpptController::data_t &mpptData) const { String serial = mpptData.serialNr_SER; @@ -226,8 +182,8 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, - const VeDirectMpptController::data_t &mpptData) +void HassIntegration::createDeviceInfo(JsonObject &object, + const VeDirectMpptController::data_t &mpptData) const { String serial = mpptData.serialNr_SER; object["name"] = "Victron(" + serial + ")"; @@ -239,9 +195,4 @@ void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, 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); -} +} // 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..8cd58baa1 --- /dev/null +++ b/src/solarcharger/victron/Stats.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#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 +{ + int32_t sum = -1; + + for (auto const& entry : _data) { + if (!entry.second) { continue; } + sum += entry.second->batteryOutputPower_W; + } + + if (sum == -1) { 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; +} + +int32_t Stats::getPanelPowerWatts() const +{ + int32_t sum = 0; + + for (auto const& entry : _data) { + if (!entry.second) { 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 = entry.second->NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0); + } + + sum += entry.second->panelPower_PPV_W; + } + + return sum; +} + +float 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; +} + +float 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, boolean fullUpdate, 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; + 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]; + publish_mppt_data(*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::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 +} + +}; // namespace SolarChargers::Victron diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index 6cb609e7d..5c081c6f1 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -6,7 +6,7 @@
-
+

{{ - $n(totalVeData.total.YieldTotal.v, 'decimal', { - minimumFractionDigits: totalVeData.total.YieldTotal.d, - maximumFractionDigits: totalVeData.total.YieldTotal.d, + $n(solarChargerData.total.YieldTotal.v, 'decimal', { + minimumFractionDigits: solarChargerData.total.YieldTotal.d, + maximumFractionDigits: solarChargerData.total.YieldTotal.d, }) }} - {{ totalVeData.total.YieldTotal.u }} + {{ solarChargerData.total.YieldTotal.u }}

-
+

{{ - $n(totalVeData.total.YieldDay.v, 'decimal', { - minimumFractionDigits: totalVeData.total.YieldDay.d, - maximumFractionDigits: totalVeData.total.YieldDay.d, + $n(solarChargerData.total.YieldDay.v, 'decimal', { + minimumFractionDigits: solarChargerData.total.YieldDay.d, + maximumFractionDigits: solarChargerData.total.YieldDay.d, }) }} - {{ totalVeData.total.YieldDay.u }} + {{ solarChargerData.total.YieldDay.u }}

-
+

{{ - $n(totalVeData.total.Power.v, 'decimal', { - minimumFractionDigits: totalVeData.total.Power.d, - maximumFractionDigits: totalVeData.total.Power.d, + $n(solarChargerData.total.Power.v, 'decimal', { + minimumFractionDigits: solarChargerData.total.Power.d, + maximumFractionDigits: solarChargerData.total.Power.d, }) }} - {{ totalVeData.total.Power.u }} + {{ solarChargerData.total.Power.u }}

@@ -194,7 +194,7 @@ diff --git a/webapp/src/views/VedirectAdminView.vue b/webapp/src/views/VedirectAdminView.vue deleted file mode 100644 index 314babe7f..000000000 --- a/webapp/src/views/VedirectAdminView.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/webapp/src/views/VedirectInfoView.vue b/webapp/src/views/VedirectInfoView.vue deleted file mode 100644 index aff6da75f..000000000 --- a/webapp/src/views/VedirectInfoView.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 545236a0b..f071c0702 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -71,7 +71,7 @@ export default defineConfig(({ command }) => { return { ws: true, changeOrigin: true }, - '^/vedirectlivedata': { + '^/solarchargerlivedata': { target: 'ws://' + proxy_target, ws: true, changeOrigin: true