Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: MQTT solarcharger #1539

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 31 additions & 9 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -182,17 +181,17 @@ 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;
float DischargeCurrentLimitBelowSoc;
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;
Expand All @@ -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 { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
char MqttOutputPowerTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttOutputPowerJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1];
WattageUnit MqttOutputPowerUnit;

enum VoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 };
char MqttOutputVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttOutputVoltageJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1];
VoltageUnit MqttOutputVoltageUnit;

enum AmperageUnit { Amps = 0, MilliAmps = 1 };
char MqttOutputCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttOutputCurrentJsonPath[MQTT_MAX_JSON_PATH_STRLEN + 1];
AmperageUnit MqttOutputCurrentUnit;
};
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;

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
18 changes: 9 additions & 9 deletions include/solarcharger/DummyStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ namespace SolarChargers {

class DummyStats : public Stats {
public:
uint32_t getAgeMillis() const override { return 0; }
std::optional<int32_t> getOutputPowerWatts() const override { return std::nullopt; }
std::optional<float> 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<float> getOutputPowerWatts() const final { return std::nullopt; }
std::optional<float> getOutputVoltage() const final { return std::nullopt; }
std::optional<uint16_t> getPanelPowerWatts() const final { return std::nullopt; }
std::optional<float> getYieldTotal() const final { return std::nullopt; }
std::optional<float> 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
8 changes: 4 additions & 4 deletions include/solarcharger/Stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ class Stats {
virtual uint32_t getAgeMillis() const;

// total output of all MPPT charge controllers in Watts
virtual std::optional<int32_t> getOutputPowerWatts() const;
virtual std::optional<float> getOutputPowerWatts() const;

// minimum of all MPPT charge controllers' output voltages in V
virtual std::optional<float> getOutputVoltage() const;

// total panel input power of all MPPT charge controllers in Watts
virtual int32_t getPanelPowerWatts() const;
virtual std::optional<uint16_t> getPanelPowerWatts() const;

// sum of total yield of all MPPT charge controllers in kWh
virtual float getYieldTotal() const;
virtual std::optional<float> getYieldTotal() const;

// sum of today's yield of all MPPT charge controllers in Wh
virtual float getYieldDay() const;
virtual std::optional<float> getYieldDay() const;

// convert stats to JSON for web application live view
virtual void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const;
Expand Down
50 changes: 50 additions & 0 deletions include/solarcharger/mqtt/Provider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <mutex>
#include <memory>
#include <TaskSchedulerDeclarations.h>
#include <solarcharger/Provider.h>
#include <solarcharger/mqtt/Stats.h>
#include <VeDirectMpptController.h>
#include <espMqttClient.h>

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<String> _subscribedTopics;
std::shared_ptr<Stats> _stats = std::make_shared<Stats>();

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
56 changes: 56 additions & 0 deletions include/solarcharger/mqtt/Stats.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <solarcharger/Stats.h>

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<float> getOutputPowerWatts() const final;
std::optional<float> getOutputVoltage() const final;
std::optional<uint16_t> getPanelPowerWatts() const final { return std::nullopt; }
std::optional<float> getYieldTotal() const final { return std::nullopt; }
std::optional<float> 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<float> 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<float> getValueIfNotOutdated(const uint32_t lastUpdate, const float value) const;
};

} // namespace SolarChargers::Mqtt
8 changes: 4 additions & 4 deletions include/solarcharger/victron/Stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ namespace SolarChargers::Victron {
class Stats : public ::SolarChargers::Stats {
public:
uint32_t getAgeMillis() const final;
std::optional<int32_t> getOutputPowerWatts() const final;
std::optional<float> getOutputPowerWatts() const final;
std::optional<float> getOutputVoltage() const final;
int32_t getPanelPowerWatts() const final;
float getYieldTotal() const final;
float getYieldDay() const final;
std::optional<uint16_t> getPanelPowerWatts() const final;
std::optional<float> getYieldTotal() const final;
std::optional<float> getYieldDay() const final;

void getLiveViewData(JsonVariant& root, const boolean fullUpdate, const uint32_t lastPublish) const final;
void mqttPublish() const final;
Expand Down
35 changes: 34 additions & 1 deletion src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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["mqtt_output_power_topic"] = source.MqttOutputPowerTopic;
target["mqtt_output_power_path"] = source.MqttOutputPowerJsonPath;
target["mqtt_output_power_unit"] = source.MqttOutputPowerUnit;
target["mqtt_output_voltage_topic"] = source.MqttOutputVoltageTopic;
target["mqtt_output_voltage_path"] = source.MqttOutputVoltageJsonPath;
target["mqtt_output_voltage_unit"] = source.MqttOutputVoltageUnit;
target["mqtt_output_current_topic"] = source.MqttOutputCurrentTopic;
target["mqtt_output_current_path"] = source.MqttOutputCurrentJsonPath;
target["mqtt_output_current_unit"] = source.MqttOutputCurrentUnit;
schlimmchen marked this conversation as resolved.
Show resolved Hide resolved
}

void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target)
{
JsonArray values = target["values"].to<JsonArray>();
Expand Down Expand Up @@ -327,6 +341,9 @@ bool ConfigurationClass::write()
JsonObject solarcharger = doc["solarcharger"].to<JsonObject>();
serializeSolarChargerConfig(config.SolarCharger, solarcharger);

JsonObject solarcharger_mqtt = solarcharger["mqtt"].to<JsonObject>();
serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, solarcharger_mqtt);

JsonObject powermeter = doc["powermeter"].to<JsonObject>();
powermeter["enabled"] = config.PowerMeter.Enabled;
powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging;
Expand Down Expand Up @@ -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.MqttOutputPowerTopic, source["mqtt_output_power_topic"] | "", sizeof(target.MqttOutputPowerTopic));
strlcpy(target.MqttOutputPowerJsonPath, source["mqtt_output_power_path"] | "", sizeof(target.MqttOutputPowerJsonPath));
target.MqttOutputPowerUnit = source["mqtt_output_power_unit"] | SolarChargerMqttConfig::WattageUnit::Watts;
strlcpy(target.MqttOutputVoltageTopic, source["mqtt_output_voltage_topic"] | "", sizeof(target.MqttOutputVoltageTopic));
strlcpy(target.MqttOutputVoltageJsonPath, source["mqtt_output_voltage_path"] | "", sizeof(target.MqttOutputVoltageJsonPath));
target.MqttOutputVoltageUnit = source["mqtt_output_voltage_unit"] | SolarChargerMqttConfig::VoltageUnit::Volts;
strlcpy(target.MqttOutputCurrentTopic, source["mqtt_output_current_topic"] | "", sizeof(target.MqttOutputCurrentTopic));
strlcpy(target.MqttOutputCurrentJsonPath, source["mqtt_output_current_path"] | "", sizeof(target.MqttOutputCurrentJsonPath));
target.MqttOutputCurrentUnit = source["mqtt_output_current_unit"] | SolarChargerMqttConfig::AmperageUnit::Amps;
}

void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target)
{
for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) {
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/WebApi_solar_charger.cpp → src/WebApi_solarcharger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ void WebApiSolarChargerlass::onAdminGet(AsyncWebServerRequest* request)

ConfigurationClass::serializeSolarChargerConfig(config.SolarCharger, root);

auto mqtt = root["mqtt"].to<JsonObject>();
ConfigurationClass::serializeSolarChargerMqttConfig(config.SolarCharger.Mqtt, mqtt);

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}

Expand Down Expand Up @@ -62,6 +65,8 @@ void WebApiSolarChargerlass::onAdminPost(AsyncWebServerRequest* request)
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
ConfigurationClass::deserializeSolarChargerConfig(root.as<JsonObject>(), config.SolarCharger);

ConfigurationClass::deserializeSolarChargerMqttConfig(root["mqtt"].as<JsonObject>(), config.SolarCharger.Mqtt);
}

WebApi.writeConfig(retMsg);
Expand Down
Loading