diff --git a/include/Configuration.h b/include/Configuration.h index 2bd984f5d..cdfd5eb54 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -29,6 +29,7 @@ #define MQTT_MAX_TOPIC_STRLEN 256 #define MQTT_MAX_LWTVALUE_STRLEN 20 #define MQTT_MAX_CERT_STRLEN 2560 +#define MQTT_MAX_JSON_PATH_STRLEN 256 #define INV_MAX_NAME_STRLEN 31 #define INV_MAX_COUNT 10 @@ -47,8 +48,6 @@ #define POWERMETER_MQTT_MAX_VALUES 3 #define POWERMETER_HTTP_JSON_MAX_VALUES 3 -#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256 -#define BATTERY_JSON_MAX_PATH_STRLEN 128 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -88,7 +87,7 @@ using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; struct POWERMETER_MQTT_VALUE_T { char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; - char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + char JsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; @@ -111,7 +110,7 @@ using PowerMeterSerialSdmConfig = struct POWERMETER_SERIAL_SDM_CONFIG_T; struct POWERMETER_HTTP_JSON_VALUE_T { HttpRequestConfig HttpRequest; bool Enabled; - char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + char JsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; @@ -182,9 +181,9 @@ struct BATTERY_CONFIG_T { uint8_t JkBmsInterface; uint8_t JkBmsPollingInterval; char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttSocJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttVoltageJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; BatteryVoltageUnit MqttVoltageUnit; bool EnableDischargeCurrentLimit; float DischargeCurrentLimit; @@ -192,7 +191,7 @@ struct BATTERY_CONFIG_T { float DischargeCurrentLimitBelowVoltage; bool UseBatteryReportedDischargeCurrentLimit; char MqttDischargeCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttDischargeCurrentJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttDischargeCurrentJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; BatteryAmperageUnit MqttAmperageUnit; }; using BatteryConfig = struct BATTERY_CONFIG_T; @@ -216,13 +215,34 @@ struct GRID_CHARGER_CONFIG_T { }; using GridChargerConfig = struct GRID_CHARGER_CONFIG_T; -enum SolarChargerProviderType { VEDIRECT = 0 }; +enum SolarChargerProviderType { VEDIRECT = 0, MQTT = 1 }; + +struct SOLARCHARGER_MQTT_CONFIG_T { + bool CalculateOutputPower; + + enum WattageUnit { KiloWatts = 0, Watts = 1, MilliWatts = 2 }; + char PowerTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char PowerJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + WattageUnit PowerUnit; + + enum VoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; + char VoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char VoltageJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + VoltageUnit VoltageTopicUnit; + + enum AmperageUnit { Amps = 0, MilliAmps = 1 }; + char CurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char CurrentJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1]; + AmperageUnit CurrentUnit; +}; +using SolarChargerMqttConfig = struct SOLARCHARGER_MQTT_CONFIG_T; struct SOLAR_CHARGER_CONFIG_T { bool Enabled; bool VerboseLogging; - SolarChargerProviderType Provider; bool PublishUpdatesOnly; + SolarChargerProviderType Provider; + SolarChargerMqttConfig Mqtt; }; using SolarChargerConfig = struct SOLAR_CHARGER_CONFIG_T; @@ -386,6 +406,7 @@ class ConfigurationClass { static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); static void serializeSolarChargerConfig(SolarChargerConfig const& source, JsonObject& target); + static void serializeSolarChargerMqttConfig(SolarChargerMqttConfig const& source, JsonObject& target); static void serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target); static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target); static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); @@ -396,6 +417,7 @@ class ConfigurationClass { static void deserializeHttpRequestConfig(JsonObject const& source_http_config, HttpRequestConfig& target); static void deserializeSolarChargerConfig(JsonObject const& source, SolarChargerConfig& target); + static void deserializeSolarChargerMqttConfig(JsonObject const& source, SolarChargerMqttConfig& target); static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target); static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); diff --git a/include/solarcharger/DummyStats.h b/include/solarcharger/DummyStats.h index bed38920f..2329fabe5 100644 --- a/include/solarcharger/DummyStats.h +++ b/include/solarcharger/DummyStats.h @@ -7,15 +7,15 @@ 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, const boolean fullUpdate, const uint32_t lastPublish) const override {} - void mqttPublish() const override {} - void mqttPublishSensors(const boolean forcePublish) const override {} + uint32_t getAgeMillis() const final { return 0; } + std::optional getOutputPowerWatts() const final { return std::nullopt; } + std::optional getOutputVoltage() const final { return std::nullopt; } + std::optional getPanelPowerWatts() const final { return std::nullopt; } + std::optional getYieldTotal() const final { return std::nullopt; } + std::optional getYieldDay() const final { return std::nullopt; } + void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final {} + void mqttPublish() const final {} + void mqttPublishSensors(const boolean forcePublish) const final {} }; } // namespace SolarChargers diff --git a/include/solarcharger/Stats.h b/include/solarcharger/Stats.h index eb9b4dc55..7f4b8cc51 100644 --- a/include/solarcharger/Stats.h +++ b/include/solarcharger/Stats.h @@ -12,19 +12,19 @@ class Stats { virtual uint32_t getAgeMillis() const; // total output of all MPPT charge controllers in Watts - virtual std::optional getOutputPowerWatts() const; + 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; + virtual std::optional getPanelPowerWatts() const; // sum of total yield of all MPPT charge controllers in kWh - virtual float getYieldTotal() const; + virtual std::optional getYieldTotal() const; // sum of today's yield of all MPPT charge controllers in Wh - virtual float getYieldDay() const; + virtual std::optional getYieldDay() const; // convert stats to JSON for web application live view virtual void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const; diff --git a/include/solarcharger/mqtt/Provider.h b/include/solarcharger/mqtt/Provider.h new file mode 100644 index 000000000..418479b1e --- /dev/null +++ b/include/solarcharger/mqtt/Provider.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace SolarChargers::Mqtt { + +class Provider : public ::SolarChargers::Provider { +public: + Provider() = default; + ~Provider() = default; + + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final { return; } // this class is event-driven + std::shared_ptr<::SolarChargers::Stats> getStats() const final { return _stats; } + +private: + Provider(Provider const& other) = delete; + Provider(Provider&& other) = delete; + Provider& operator=(Provider const& other) = delete; + Provider& operator=(Provider&& other) = delete; + + bool _verboseLogging = false; + String _outputPowerTopic; + String _outputVoltageTopic; + String _outputCurrentTopic; + std::vector _subscribedTopics; + std::shared_ptr _stats = std::make_shared(); + + void onMqttMessageOutputPower(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; + + void onMqttMessageOutputVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; + + void onMqttMessageOutputCurrent(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const; +}; + +} // namespace SolarChargers::Mqtt diff --git a/include/solarcharger/mqtt/Stats.h b/include/solarcharger/mqtt/Stats.h new file mode 100644 index 000000000..563802ed3 --- /dev/null +++ b/include/solarcharger/mqtt/Stats.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +namespace SolarChargers::Mqtt { + +class Stats : public ::SolarChargers::Stats { +friend class Provider; + +public: + // the last time *any* data was updated + uint32_t getAgeMillis() const final { return millis() - _lastUpdate; } + + std::optional getOutputPowerWatts() const final; + std::optional getOutputVoltage() const final; + std::optional getPanelPowerWatts() const final { return std::nullopt; } + std::optional getYieldTotal() const final { return std::nullopt; } + std::optional getYieldDay() const final { return std::nullopt; } + + void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final; + + // no need to republish values received via mqtt + void mqttPublish() const final {} + + // no need to republish values received via mqtt + void mqttPublishSensors(const boolean forcePublish) const final {} + +protected: + std::optional getOutputCurrent() const; + + void setOutputPowerWatts(const float powerWatts) { + _outputPowerWatts = powerWatts; + _lastUpdateOutputPowerWatts = _lastUpdate = millis(); + } + + void setOutputVoltage(const float voltage); + + void setOutputCurrent(const float current); + +private: + uint32_t _lastUpdate = 0; + + float _outputPowerWatts = 0; + uint32_t _lastUpdateOutputPowerWatts = 0; + + float _outputVoltage = 0; + uint32_t _lastUpdateOutputVoltage = 0; + + float _outputCurrent = 0; + uint32_t _lastUpdateOutputCurrent = 0; + + std::optional getValueIfNotOutdated(const uint32_t lastUpdate, const float value) const; +}; + +} // namespace SolarChargers::Mqtt diff --git a/include/solarcharger/victron/Stats.h b/include/solarcharger/victron/Stats.h index cb4750c6e..e54ff6ba1 100644 --- a/include/solarcharger/victron/Stats.h +++ b/include/solarcharger/victron/Stats.h @@ -11,11 +11,11 @@ namespace SolarChargers::Victron { class Stats : public ::SolarChargers::Stats { public: uint32_t getAgeMillis() const final; - std::optional getOutputPowerWatts() 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; + std::optional getPanelPowerWatts() const final; + std::optional getYieldTotal() const final; + std::optional getYieldDay() const final; void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final; void mqttPublish() const final; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index be45a29de..3c9dfb27b 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -59,6 +59,20 @@ void ConfigurationClass::serializeSolarChargerConfig(SolarChargerConfig const& s target["publish_updates_only"] = source.PublishUpdatesOnly; } +void ConfigurationClass::serializeSolarChargerMqttConfig(SolarChargerMqttConfig const& source, JsonObject& target) +{ + target["calculate_output_power"] = source.CalculateOutputPower; + target["power_topic"] = source.PowerTopic; + target["power_path"] = source.PowerJsonPath; + target["power_unit"] = source.PowerUnit; + target["voltage_topic"] = source.VoltageTopic; + target["voltage_path"] = source.VoltageJsonPath; + target["voltage_unit"] = source.VoltageTopicUnit; + target["current_topic"] = source.CurrentTopic; + target["current_path"] = source.CurrentJsonPath; + target["current_unit"] = source.CurrentUnit; +} + void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target) { JsonArray values = target["values"].to(); @@ -327,6 +341,9 @@ bool ConfigurationClass::write() JsonObject solarcharger = doc["solarcharger"].to(); serializeSolarChargerConfig(config.SolarCharger, solarcharger); + JsonObject solarcharger_mqtt = solarcharger["mqtt"].to(); + serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, solarcharger_mqtt); + JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; @@ -386,6 +403,20 @@ void ConfigurationClass::deserializeSolarChargerConfig(JsonObject const& source, target.PublishUpdatesOnly = source["publish_updates_only"] | SOLAR_CHARGER_PUBLISH_UPDATES_ONLY; } +void ConfigurationClass::deserializeSolarChargerMqttConfig(JsonObject const& source, SolarChargerMqttConfig& target) +{ + target.CalculateOutputPower = source["calculate_output_power"]; + strlcpy(target.PowerTopic, source["power_topic"] | "", sizeof(target.PowerTopic)); + strlcpy(target.PowerJsonPath, source["power_path"] | "", sizeof(target.PowerJsonPath)); + target.PowerUnit = source["power_unit"] | SolarChargerMqttConfig::WattageUnit::Watts; + strlcpy(target.VoltageTopic, source["voltage_topic"] | "", sizeof(target.VoltageTopic)); + strlcpy(target.VoltageJsonPath, source["voltage_path"] | "", sizeof(target.VoltageJsonPath)); + target.VoltageTopicUnit = source["voltage_unit"] | SolarChargerMqttConfig::VoltageUnit::Volts; + strlcpy(target.CurrentTopic, source["current_topic"] | "", sizeof(target.CurrentTopic)); + strlcpy(target.CurrentJsonPath, source["current_path"] | "", sizeof(target.CurrentJsonPath)); + target.CurrentUnit = source["current_unit"] | SolarChargerMqttConfig::AmperageUnit::Amps; +} + void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) { for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { @@ -694,7 +725,9 @@ bool ConfigurationClass::read() } } - deserializeSolarChargerConfig(doc["solarcharger"], config.SolarCharger); + JsonObject solarcharger = doc["solarcharger"]; + deserializeSolarChargerConfig(solarcharger, config.SolarCharger); + deserializeSolarChargerMqttConfig(solarcharger["mqtt"], config.SolarCharger.Mqtt); JsonObject powermeter = doc["powermeter"]; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; diff --git a/src/WebApi_solar_charger.cpp b/src/WebApi_solarcharger.cpp similarity index 90% rename from src/WebApi_solar_charger.cpp rename to src/WebApi_solarcharger.cpp index 454d0f45a..72015b55d 100644 --- a/src/WebApi_solar_charger.cpp +++ b/src/WebApi_solarcharger.cpp @@ -31,6 +31,9 @@ void WebApiSolarChargerlass::onAdminGet(AsyncWebServerRequest* request) ConfigurationClass::serializeSolarChargerConfig(config.SolarCharger, root); + auto mqtt = root["mqtt"].to(); + ConfigurationClass::serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, mqtt); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -62,6 +65,8 @@ void WebApiSolarChargerlass::onAdminPost(AsyncWebServerRequest* request) auto guard = Configuration.getWriteGuard(); auto& config = guard.getConfig(); ConfigurationClass::deserializeSolarChargerConfig(root.as(), config.SolarCharger); + + ConfigurationClass::deserializeSolarChargerMqttConfig(root["mqtt"].as(), config.SolarCharger.Mqtt); } WebApi.writeConfig(retMsg); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 2b08258ce..1eddcb277 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -82,19 +82,29 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al solarchargerObj["enabled"] = config.SolarCharger.Enabled; if (config.SolarCharger.Enabled) { - auto totalVeObj = solarchargerObj["total"].to(); - - auto power = SolarCharger.getStats()->getPanelPowerWatts(); + float power = 0; auto outputPower = SolarCharger.getStats()->getOutputPowerWatts(); + auto panelPower = SolarCharger.getStats()->getPanelPowerWatts(); - // 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 (power == 0 && panelPower) { + power = *panelPower; + } + + addTotalField(solarchargerObj, "power", power, "W", 1); + + auto yieldDay = SolarCharger.getStats()->getYieldDay(); + if (yieldDay) { + addTotalField(solarchargerObj, "yieldDay", *yieldDay, "Wh", 0); + } + + auto yieldTotal = SolarCharger.getStats()->getYieldTotal(); + if (yieldTotal) { + addTotalField(solarchargerObj, "yieldTotal", *yieldTotal, "kWh", 2); + } } if (!all) { _lastPublishSolarCharger = millis(); } diff --git a/src/solarcharger/Controller.cpp b/src/solarcharger/Controller.cpp index cae332463..1efc62d09 100644 --- a/src/solarcharger/Controller.cpp +++ b/src/solarcharger/Controller.cpp @@ -5,6 +5,7 @@ #include #include #include +#include SolarChargers::Controller SolarCharger; @@ -38,6 +39,9 @@ void Controller::updateSettings() case SolarChargerProviderType::VEDIRECT: _upProvider = std::make_unique<::SolarChargers::Victron::Provider>(); break; + case SolarChargerProviderType::MQTT: + _upProvider = std::make_unique<::SolarChargers::Mqtt::Provider>(); + break; default: MessageOutput.printf("[SolarCharger] Unknown provider: %d\r\n", config.SolarCharger.Provider); return; diff --git a/src/solarcharger/mqtt/Provider.cpp b/src/solarcharger/mqtt/Provider.cpp new file mode 100644 index 000000000..61390c0f4 --- /dev/null +++ b/src/solarcharger/mqtt/Provider.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include +#include + +namespace SolarChargers::Mqtt { + +bool Provider::init(bool verboseLogging) +{ + _verboseLogging = verboseLogging; + auto const& config = Configuration.get().SolarCharger.Mqtt; + + _outputPowerTopic = config.PowerTopic; + _outputCurrentTopic = config.CurrentTopic; + + auto configValid = config.CalculateOutputPower ? !_outputCurrentTopic.isEmpty() : !_outputPowerTopic.isEmpty(); + + if (!configValid) { + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Init failed. calculateOuputPower %s, %s topic is empty\r\n", + config.CalculateOutputPower ? "enabled" : "disabled", + config.CalculateOutputPower ? "output_current" : "output_power"); + } + return false; + } + + if (!_outputPowerTopic.isEmpty() + && !config.CalculateOutputPower) { + _subscribedTopics.push_back(_outputPowerTopic); + + MqttSettings.subscribe(_outputPowerTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputPower, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.PowerJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for ouput_power readings\r\n", + _outputPowerTopic.c_str()); + } + } + + if (!_outputCurrentTopic.isEmpty()) { + _subscribedTopics.push_back(_outputCurrentTopic); + + MqttSettings.subscribe(_outputCurrentTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputCurrent, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.CurrentJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for output_current readings\r\n", + _outputCurrentTopic.c_str()); + } + } + + _outputVoltageTopic = config.VoltageTopic; + if (!_outputVoltageTopic.isEmpty()) { + _subscribedTopics.push_back(_outputVoltageTopic); + + MqttSettings.subscribe(_outputVoltageTopic, 0/*QoS*/, + std::bind(&Provider::onMqttMessageOutputVoltage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.VoltageJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Subscribed to '%s' for ouput_voltage readings\r\n", + _outputVoltageTopic.c_str()); + } + } + + return true; +} + +void Provider::deinit() +{ + for (auto const& topic : _subscribedTopics) { + MqttSettings.unsubscribe(topic); + } + _subscribedTopics.clear(); +} + +void Provider::onMqttMessageOutputPower(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputPower = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputPower.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::WattageUnit; + switch (config.PowerUnit) { + case Unit_t::MilliWatts: + *outputPower /= 1000; + break; + case Unit_t::KiloWatts: + *outputPower *= 1000; + break; + default: + break; + } + + if (*outputPower < 0) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_power '%.1f' in topic '%s'\r\n", + *outputPower, topic); + return; + } + + _stats->setOutputPowerWatts(*outputPower); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_power to %.1f from '%s'\r\n", + *outputPower, topic); + } +} + +void Provider::onMqttMessageOutputVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputVoltage = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputVoltage.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::VoltageUnit; + switch (config.VoltageTopicUnit) { + case Unit_t::DeciVolts: + *outputVoltage /= 10; + break; + case Unit_t::CentiVolts: + *outputVoltage /= 100; + break; + case Unit_t::MilliVolts: + *outputVoltage /= 1000; + break; + default: + break; + } + + // since this project is revolving around Hoymiles microinverters, which can + // only handle up to 65V of input voltage at best, it is safe to assume that + // an even higher voltage is implausible. + if (*outputVoltage < 0 || *outputVoltage > 65) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_voltage '%.2f' in topic '%s'\r\n", + *outputVoltage, topic); + return; + } + + _stats->setOutputVoltage(*outputVoltage); + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_voltage to %.2f from '%s'\r\n", + *outputVoltage, topic); + } +} + +void Provider::onMqttMessageOutputCurrent(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) const +{ + auto outputCurrent = Utils::getNumericValueFromMqttPayload("SolarChargers::Mqtt", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!outputCurrent.has_value()) { return; } + + auto const& config = Configuration.get().SolarCharger.Mqtt; + using Unit_t = SolarChargerMqttConfig::AmperageUnit; + switch (config.CurrentUnit) { + case Unit_t::MilliAmps: + *outputCurrent /= 1000; + break; + default: + break; + } + + _stats->setOutputCurrent(*outputCurrent); + + if (*outputCurrent < 0) { + MessageOutput.printf("[SolarChargers::Mqtt]: Implausible output_current '%.2f' in topic '%s'\r\n", + *outputCurrent, topic); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[SolarChargers::Mqtt]: Updated output_current to %.2f from '%s'\r\n", + *outputCurrent, topic); + } +} + +} // namespace SolarChargers::Mqtt diff --git a/src/solarcharger/mqtt/Stats.cpp b/src/solarcharger/mqtt/Stats.cpp new file mode 100644 index 000000000..a935c2c63 --- /dev/null +++ b/src/solarcharger/mqtt/Stats.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include + +namespace SolarChargers::Mqtt { + +std::optional Stats::getOutputPowerWatts() const +{ + return getValueIfNotOutdated(_lastUpdateOutputPowerWatts, _outputPowerWatts); +} + +std::optional Stats::getOutputVoltage() const +{ + return getValueIfNotOutdated(_lastUpdateOutputVoltage, _outputVoltage); +} + +std::optional Stats::getOutputCurrent() const { + return getValueIfNotOutdated(_lastUpdateOutputCurrent, _outputCurrent); +} + +void Stats::setOutputVoltage(const float voltage) { + _outputVoltage = voltage; + _lastUpdateOutputVoltage = _lastUpdate = millis(); + + auto outputCurrent = getOutputCurrent(); + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower + && outputCurrent) { + setOutputPowerWatts(voltage * *outputCurrent); + } +} + +void Stats::setOutputCurrent(const float current) { + _outputCurrent = current; + _lastUpdateOutputCurrent = _lastUpdate = millis(); + + auto outputVoltage = getOutputVoltage(); + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower + && outputVoltage) { + setOutputPowerWatts(*outputVoltage * current); + } +} + +std::optional Stats::getValueIfNotOutdated(const uint32_t lastUpdate, const float value) const { + // never updated or older than 60 seconds + if (lastUpdate == 0 + || millis() - lastUpdate > 60 * 1000) { + return std::nullopt; + } + + return value; +} + +void Stats::getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const +{ + ::SolarChargers::Stats::getLiveViewData(root, fullUpdate, lastPublish); + + auto age = 0; + if (_lastUpdate) { + age = millis() - _lastUpdate; + } + + auto hasUpdate = age != 0 && age < millis() - lastPublish; + if (!fullUpdate && !hasUpdate) { return; } + + const JsonObject instance = root["solarcharger"]["instances"]["MQTT"].to(); + instance["data_age_ms"] = age; + instance["hide_serial"] = true; + instance["product_id"] = "MPPT: MQTT"; + + const JsonObject output = instance["values"]["output"].to(); + + if (Configuration.get().SolarCharger.Mqtt.CalculateOutputPower) { + output["P"]["v"] = _outputPowerWatts; + output["P"]["u"] = "W"; + output["P"]["d"] = 1; + output["V"]["v"] = _outputVoltage; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + output["I"]["v"] = _outputCurrent; + output["I"]["u"] = "A"; + output["I"]["d"] = 2; + } + else { + output["Power"]["v"] = _outputPowerWatts; + output["Power"]["u"] = "W"; + output["Power"]["d"] = 1; + + auto outputVoltage = getOutputVoltage(); + if (outputVoltage) { + output["V"]["v"] = *outputVoltage; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + } + } +} + +}; // namespace SolarChargers::Mqtt diff --git a/src/solarcharger/victron/Stats.cpp b/src/solarcharger/victron/Stats.cpp index c50f9805f..3f968f560 100644 --- a/src/solarcharger/victron/Stats.cpp +++ b/src/solarcharger/victron/Stats.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include namespace SolarChargers::Victron { @@ -32,9 +31,9 @@ uint32_t Stats::getAgeMillis() const } -std::optional Stats::getOutputPowerWatts() const +std::optional Stats::getOutputPowerWatts() const { - int32_t sum = 0; + float sum = 0; auto data = false; for (auto const& entry : _data) { @@ -65,12 +64,14 @@ std::optional Stats::getOutputVoltage() const return min; } -int32_t Stats::getPanelPowerWatts() const +std::optional Stats::getPanelPowerWatts() const { - int32_t sum = 0; + uint16_t sum = 0; + auto data = false; for (auto const& entry : _data) { if (!entry.second) { continue; } + data = true; // if any charge controller is part of a VE.Smart network, and if the // charge controller is connected in a way that allows to send @@ -83,10 +84,12 @@ int32_t Stats::getPanelPowerWatts() const sum += entry.second->panelPower_PPV_W; } + if (!data) { return std::nullopt; } + return sum; } -float Stats::getYieldTotal() const +std::optional Stats::getYieldTotal() const { float sum = 0; @@ -99,7 +102,7 @@ float Stats::getYieldTotal() const return sum; } -float Stats::getYieldDay() const +std::optional Stats::getYieldDay() const { float sum = 0; @@ -131,6 +134,7 @@ void Stats::getLiveViewData(JsonVariant& root, const boolean fullUpdate, const u JsonObject instance = instances[entry.first].to(); instance["data_age_ms"] = age; + instance["hide_serial"] = false; populateJsonWithInstanceStats(instance, *entry.second); } } diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index e9ab89c6d..5347e0303 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -6,49 +6,55 @@
-
- -

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

-
-
-
- -

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

-
-
-
- -

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

-
-
+
{{ item.product_id }}
-
+
{{ $t('solarchargerhome.SerialNumber') }}: {{ serial }}
-
+
{{ $t('solarchargerhome.FirmwareVersion') }}: {{ item.firmware_version }}
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 804c1bd42..5b3f5facd 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -38,7 +38,10 @@ "Release": "Loslassen zum Aktualisieren", "Close": "Schließen", "Yes": "Ja", - "No": "Nein" + "No": "Nein", + "Unit": "Einheit", + "MqttJsonPath": "Optional: JSON-Pfad", + "MqttJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Wert in den JSON Nutzdatzen zu finden, z.B. 'electricLevel'. Leer lassen, falls die Nutzdaten des Topics einen numerischen Wert enthält." }, "wait": { "NotReady": "OpenDTU-OnBattery ist noch nicht bereit", @@ -175,7 +178,7 @@ "Seconds": "vor {val} Sekunden", "Property": "Eigenschaft", "Value": "Wert", - "Unit": "Einheit", + "Unit": "@:base.Unit", "section_device": "Geräteinformation", "device": { "LOAD": "Status Lastausgang", @@ -197,7 +200,8 @@ "E": "Effizienz (berechnet)", "AbsorptionVoltage": "Absorptionsspannung", "FloatVoltage": "Erhaltungsspannung", - "SBSTemperature": "SBS Temperatur" + "SBSTemperature": "SBS Temperatur", + "Power": "Leistung" }, "section_input": "Eingang (Solarpanele)", "input": { @@ -595,8 +599,25 @@ "EnableSolarCharger": "Aktiviere Schnittstelle", "Provider": "Datenanbieter", "ProviderVeDirect": "Victron MPPT(s) per VE.Direct Schnittstelle", + "ProviderMqtt": "Solarladereglerwerte aus MQTT Broker", "VerboseLogging": "@:base.VerboseLogging", - "MqttPublishUpdatesOnly": "Werte nur bei Änderung an MQTT broker senden" + "MqttPublishUpdatesOnly": "Werte nur bei Änderung an MQTT broker senden", + "CalculateOutputPower": "Solarladeregler-Ausgangsleistung berechnen", + "CalculateOutputPowerDescription": "Wenn aktiviert, wird die Ausgangsleistung des Solarladeregler basierend auf den Strom- und Spannungswerten berechnet. Wenn deaktiviert, wird die Ausgangsleistung direkt vom Solarladeregler übernommen.", + "OutputPowerUsageHint": "Hinweis: Die Ausgangsleistung wird von der DPL-Solar-Passthrough-Funktion verwendet. Weitere Details findest du in der Solar-Passthrough Dokumentation.", + "MqttJsonPath": "@:base.MqttJsonPath", + "MqttJsonPathDescription": "@:base.MqttJsonPathDescription", + "MqttOutputPowerConfiguration": "Einstellungen Ausgangsleistung", + "MqttOutputPowerTopic": "Ausgangsleistung Topic", + "MqttOutputPowerUnit": "@:base.Unit", + "MqttOutputCurrentConfiguration": "Einstellungen Ausgangsstrom", + "MqttOutputCurrentUsageHint": "Der Ausgangsstrom wird verwendet, um die Ausgangsleistung basierend auf den Strom- und Spannungswerten zu berechnen, wenn 'Solarladeregler-Ausgangsleistung berechnen' aktiviert ist.", + "MqttOutputCurrentTopic": "Ausgangsstrom Topic", + "MqttOutputCurrentUnit": "@:base.Unit", + "MqttOutputVoltageConfiguration": "Einstellungen Ausgangsspannung", + "MqttOutputVoltagetUsageHint": "Die Ausgangsspannung wird als Ersatzspannung für die DPL verwendet, wenn die Batteriespannung nicht bekannt ist, und ist erforderlich, wenn 'Solarladeregler-Ausgangsleistung berechnen' aktiviert ist. Weitere Details findest du in der Solar-Passthrough Dokumentation.", + "MqttOutputVoltageTopic": "Ausgangsspannung Topic", + "MqttOutputVoltageUnit": "@:base.Unit" }, "powermeteradmin": { "PowerMeterSettings": "Stromzähler Einstellungen", @@ -627,7 +648,7 @@ "httpEnabled": "Wert aktiviert", "valueJsonPath": "JSON-Pfad", "valueJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in den JSON Nutzdatzen zu finden, z.B. 'power/total/watts' oder nur 'total'.", - "valueUnit": "Einheit", + "valueUnit": "@:base.Unit", "valueSignInverted": "Vorzeichen umkehren", "valueSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "testHttpJsonHeader": "Konfiguration testen", @@ -727,9 +748,9 @@ "ProviderPytesCan": "Pytes per CAN-Bus", "MqttSocConfiguration": "Einstellungen SoC", "MqttVoltageConfiguration": "Einstellungen Spannung", - "MqttJsonPath": "Optional: JSON-Pfad", - "MqttJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Wert in den JSON Nutzdatzen zu finden, z.B. 'electricLevel'. Leer lassen, falls die Nutzdaten des Topics einen numerischen Wert enthält.", - "MqttVoltageUnit": "Einheit", + "MqttJsonPath": "@:base.MqttJsonPath", + "MqttJsonPathDescription": "@:base.MqttJsonPathDescription", + "MqttVoltageUnit": "@:base.Unit", "MqttSocTopic": "Topic für SoC", "MqttVoltageTopic": "Topic für Spannung", "SerialSettings": "Einstellungen Serielle Schnittstelle", @@ -749,7 +770,7 @@ "UseBatteryReportedDischargeCurrentLimit": "Von der Batterie übermitteltes Limit verwenden", "BatteryReportedDischargeCurrentLimitInfo": "Hinweis: Das niedrigste Limit wird angewendet, wobei das von der Batterie übermittelte Entladestromlimit nur verwendet wird, wenn in der letzten Minute ein Update eingegangen ist; andernfalls dient das zuvor festgelegte Limit als Fallback.", "MqttDischargeCurrentTopic": "Topic für Entladestromlimit", - "MqttAmperageUnit": "Einheit" + "MqttAmperageUnit": "@:base.Unit" }, "inverteradmin": { "InverterSettings": "Wechselrichter Einstellungen", @@ -924,7 +945,7 @@ "Output": "Ausgang", "Property": "Eigenschaft", "Value": "Wert", - "Unit": "Einheit", + "Unit": "@:base.Unit", "input_voltage": "Eingangsspannung", "input_current": "Eingangsstrom", "input_power": "Eingangsleistung", @@ -984,7 +1005,7 @@ "yes": "@:base.Yes", "no": "@:base.No", "Value": "Wert", - "Unit": "Einheit", + "Unit": "@:base.Unit", "SoC": "Ladezustand (SoC)", "stateOfHealth": "Batteriezustand (SoH)", "voltage": "Spannung", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index ba0459992..f8a2238ad 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -38,7 +38,10 @@ "Release": "Release to refresh", "Close": "Close", "Yes": "Yes", - "No": "No" + "No": "No", + "Unit": "Unit", + "MqttJsonPath": "Optional: JSON Path", + "MqttJsonPathDescription": "Application specific JSON path to find the value in the JSON payload, e.g., 'electricLevel'. Leave empty if the topic's payload contains a plain numeric value." }, "wait": { "NotReady": "OpenDTU-OnBattery is not yet ready", @@ -175,7 +178,7 @@ "Seconds": "{val} seconds", "Property": "Property", "Value": "Value", - "Unit": "Unit", + "Unit": "@:base.Unit", "section_device": "Device Info", "device": { "LOAD": "Load output state", @@ -197,7 +200,8 @@ "E": "Efficiency (calculated)", "AbsorptionVoltage": "Absorption voltage", "FloatVoltage": "Float voltage", - "SBSTemperature": "SBS temperature" + "SBSTemperature": "SBS temperature", + "Power": "Power" }, "section_input": "Input (Solar Panels)", "input": { @@ -597,8 +601,25 @@ "EnableSolarCharger": "Enable Interface", "Provider": "Data Provider", "ProviderVeDirect": "Victron MPPT(s) using VE.Direct interface", + "ProviderMqtt": "Solar Charger data from MQTT broker", "VerboseLogging": "@:base.VerboseLogging", - "MqttPublishUpdatesOnly": "Publish values to MQTT only when they change" + "MqttPublishUpdatesOnly": "Publish values to MQTT only when they change", + "CalculateOutputPower": "Calculate Solar Charger output power", + "CalculateOutputPowerDescription": "If enabled, the output power of the Solar Charger will be calculated based on the current and voltage values. If disabled, the output power will be taken from the Solar Charger directly.", + "OutputPowerUsageHint": "Hint: The output power is used by the DPL solar-passthrough feature. Details can be found in the documentation concerning Solar-Passthrough.", + "MqttJsonPath": "@:base.MqttJsonPath", + "MqttJsonPathDescription": "@:base.MqttJsonPathDescription", + "MqttOutputPowerConfiguration": "Output Power Settings", + "MqttOutputPowerTopic": "Output Power Topic", + "MqttOutputPowerUnit": "@:base.Unit", + "MqttOutputCurrentConfiguration": "Output Current Settings", + "MqttOutputCurrentUsageHint": "The output current is used to calculate the output power based on the current and voltage values when 'Calculate Solar Charger output power' is enabled.", + "MqttOutputCurrentTopic": "Output Current Topic", + "MqttOutputCurrentUnit": "@:base.Unit", + "MqttOutputVoltageConfiguration": "Output Voltage Settings", + "MqttOutputVoltagetUsageHint": "The output voltage is used as a fallback voltage for the DPL when the battery voltage is not known, and is needed when 'Calculate Solar Charger output power' is enabled. Details can be found in the documentation concerning Solar-Passthrough.", + "MqttOutputVoltageTopic": "Output Voltage Topic", + "MqttOutputVoltageUnit": "@:base.Unit" }, "powermeteradmin": { "PowerMeterSettings": "Power Meter Settings", @@ -629,7 +650,7 @@ "httpEnabled": "Value Enabled", "valueJsonPath": "JSON Path", "valueJsonPathDescription": "Application specific JSON path to find the power value in the JSON payload, e.g., 'power/total/watts' or simply 'total'.", - "valueUnit": "Unit", + "valueUnit": "@:base.Unit", "valueSignInverted": "Change Sign", "valueSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", "testHttpJsonHeader": "Test Configuration", @@ -730,9 +751,9 @@ "MqttConfiguration": "MQTT Settings", "MqttSocConfiguration": "SoC Settings", "MqttVoltageConfiguration": "Voltage Settings", - "MqttJsonPath": "Optional: JSON Path", - "MqttJsonPathDescription": "Application specific JSON path to find the value in the JSON payload, e.g., 'electricLevel'. Leave empty if the topic's payload contains a plain numeric value.", - "MqttVoltageUnit": "Unit", + "MqttJsonPath": "@:base.MqttJsonPath", + "MqttJsonPathDescription": "@:base.MqttJsonPathDescription", + "MqttVoltageUnit": "@:base.Unit", "MqttSocTopic": "SoC Value Topic", "MqttVoltageTopic": "Voltage Value Topic", "SerialSettings": "Serial Settings", @@ -752,7 +773,7 @@ "UseBatteryReportedDischargeCurrentLimit": "Use Battery-Reported limit", "BatteryReportedDischargeCurrentLimitInfo": "Hint: The lowest limit will be applied, with the battery-reported discharge current limit used only if an update was received in the last minute; otherwise, the previously specified limit will act as a fallback.", "MqttDischargeCurrentTopic": "Discharge Current Limit Value Topic", - "MqttAmperageUnit": "Unit" + "MqttAmperageUnit": "@:base.Unit" }, "inverteradmin": { "InverterSettings": "Inverter Settings", @@ -928,7 +949,7 @@ "Output": "Output", "Property": "Property", "Value": "Value", - "Unit": "Unit", + "Unit": "@:base.Unit", "input_voltage": "Input voltage", "input_current": "Input current", "input_power": "Input power", @@ -988,7 +1009,7 @@ "yes": "@:base.Yes", "no": "@:base.No", "Value": "Value", - "Unit": "Unit", + "Unit": "@:base.Unit", "SoC": "State of Charge", "stateOfHealth": "State of Health", "voltage": "Voltage", diff --git a/webapp/src/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index 33904921e..0bbd028a2 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -63,7 +63,9 @@ export interface Hints { export interface SolarCharger { enabled: boolean; - total: Total; + power?: ValueObject; + yieldDay?: ValueObject; + yieldTotal?: ValueObject; } export interface Huawei { diff --git a/webapp/src/types/SolarChargerConfig.ts b/webapp/src/types/SolarChargerConfig.ts index e4212b078..e63ff7928 100644 --- a/webapp/src/types/SolarChargerConfig.ts +++ b/webapp/src/types/SolarChargerConfig.ts @@ -1,6 +1,20 @@ +export interface SolarChargerMqttConfig { + calculate_output_power: boolean; + mqtt_output_power_topic: string; + mqtt_output_power_path: string; + mqtt_output_power_unit: number; + mqtt_output_voltage_topic: string; + mqtt_output_voltage_path: string; + mqtt_output_voltage_unit: number; + mqtt_output_current_topic: string; + mqtt_output_current_path: string; + mqtt_output_current_unit: number; +} + export interface SolarChargerConfig { enabled: boolean; verbose_logging: boolean; provider: number; publish_updates_only: boolean; + mqtt: SolarChargerMqttConfig; } diff --git a/webapp/src/types/SolarChargerLiveDataStatus.ts b/webapp/src/types/SolarChargerLiveDataStatus.ts index 83d57dd29..b45aa7365 100644 --- a/webapp/src/types/SolarChargerLiveDataStatus.ts +++ b/webapp/src/types/SolarChargerLiveDataStatus.ts @@ -15,6 +15,7 @@ type MpptData = (ValueObject | string)[]; export interface SolarChargerInstance { data_age_ms: number; product_id: string; - firmware_version: string; + firmware_version?: string; + hide_serial: boolean; values: { [key: string]: MpptData }; } diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index 6652e6fc8..57196ae1d 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -85,7 +85,7 @@ :label="$t('batteryadmin.MqttJsonPath')" v-model="batteryConfigList.mqtt_soc_json_path" type="text" - maxlength="128" + maxlength="256" :tooltip="$t('batteryadmin.MqttJsonPathDescription')" wide /> @@ -104,7 +104,7 @@ :label="$t('batteryadmin.MqttJsonPath')" v-model="batteryConfigList.mqtt_voltage_json_path" type="text" - maxlength="128" + maxlength="256" :tooltip="$t('batteryadmin.MqttJsonPathDescription')" wide /> @@ -215,7 +215,7 @@ v-model="batteryConfigList.mqtt_discharge_current_json_path" wide type="text" - maxlength="128" + maxlength="256" :tooltip="$t('batteryadmin.MqttJsonPathDescription')" /> diff --git a/webapp/src/views/SolarChargerAdminView.vue b/webapp/src/views/SolarChargerAdminView.vue index b4b901ced..46944b8d8 100644 --- a/webapp/src/views/SolarChargerAdminView.vue +++ b/webapp/src/views/SolarChargerAdminView.vue @@ -41,9 +41,156 @@ type="checkbox" wide /> + + + + @@ -74,7 +221,25 @@ export default defineComponent({ alertMessage: '', alertType: 'info', showAlert: false, - providerTypeList: [{ key: 0, value: 'VeDirect' }], + providerTypeList: [ + { key: 0, value: 'VeDirect' }, + { key: 1, value: 'Mqtt' }, + ], + wattageUnitTypeList: [ + { key: 0, value: 'kW' }, + { key: 1, value: 'W' }, + { key: 2, value: 'mW' }, + ], + voltageUnitTypeList: [ + { key: 0, value: 'V' }, + { key: 1, value: 'dV' }, + { key: 2, value: 'cV' }, + { key: 3, value: 'mV' }, + ], + amperageUnitTypeList: [ + { key: 0, value: 'A' }, + { key: 1, value: 'mA' }, + ], }; }, created() {