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

DPL Voltage Features #679

Merged
merged 6 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 26 additions & 11 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ class BatteryStats {
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
bool updateAvailable(uint32_t since) const { return _lastUpdate > since; }

uint8_t getSoC() const { return _SoC; }
uint8_t getSoC() const { return _soc; }
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }

float getVoltage() const { return _voltage; }
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }

// convert stats to JSON for web application live view
virtual void getLiveViewData(JsonVariant& root) const;

Expand All @@ -29,18 +32,33 @@ class BatteryStats {
// if they did not change. used to calculate Home Assistent expiration.
virtual uint32_t getMqttFullPublishIntervalMs() const;

bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; }
bool isSoCValid() const { return _lastUpdateSoC > 0; }
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }

protected:
virtual void mqttPublish() const;

void setSoC(float soc, uint8_t precision, uint32_t timestamp) {
_soc = soc;
_socPrecision = precision;
_lastUpdateSoC = timestamp;
}

void setVoltage(float voltage, uint32_t timestamp) {
_voltage = voltage;
_lastUpdateVoltage = timestamp;
}

String _manufacturer = "unknown";
uint8_t _SoC = 0;
uint32_t _lastUpdateSoC = 0;
uint32_t _lastUpdate = 0;

private:
uint32_t _lastMqttPublish = 0;
float _soc = 0;
uint8_t _socPrecision = 0; // decimal places
uint32_t _lastUpdateSoC = 0;
float _voltage = 0; // total battery pack voltage
uint32_t _lastUpdateVoltage = 0;
};

class PylontechBatteryStats : public BatteryStats {
Expand All @@ -52,14 +70,12 @@ class PylontechBatteryStats : public BatteryStats {

private:
void setManufacturer(String&& m) { _manufacturer = std::move(m); }
void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = millis(); }
void setLastUpdate(uint32_t ts) { _lastUpdate = ts; }

float _chargeVoltage;
float _chargeCurrentLimitation;
float _dischargeCurrentLimitation;
uint16_t _stateOfHealth;
float _voltage; // total voltage of the battery pack
// total current into (positive) or from (negative)
// the battery, i.e., the charging current
float _current;
Expand Down Expand Up @@ -123,7 +139,6 @@ class VictronSmartShuntStats : public BatteryStats {
void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData);

private:
float _voltage;
float _current;
float _temperature;
bool _tempPresent;
Expand All @@ -141,14 +156,14 @@ class VictronSmartShuntStats : public BatteryStats {
};

class MqttBatteryStats : public BatteryStats {
friend class MqttBattery;

public:
// since the source of information was MQTT in the first place,
// we do NOT publish the same data under a different topic.
void mqttPublish() const final { }

// the SoC is the only interesting value in this case, which is already
// displayed at the top of the live view. do not generate a card.
// if the voltage is subscribed to at all, it alone does not warrant a
// card in the live view, since the SoC is already displayed at the top
void getLiveViewData(JsonVariant& root) const final { }

void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = _lastUpdate = millis(); }
};
4 changes: 3 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ struct CONFIG_T {
int32_t TargetPowerConsumptionHysteresis;
int32_t LowerPowerLimit;
int32_t UpperPowerLimit;
bool IgnoreSoc;
uint32_t BatterySocStartThreshold;
uint32_t BatterySocStopThreshold;
float VoltageStartThreshold;
Expand All @@ -230,7 +231,8 @@ struct CONFIG_T {
uint8_t Provider;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery;

struct {
Expand Down
7 changes: 6 additions & 1 deletion include/MqttBattery.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <optional>
#include "Battery.h"
#include <espMqttClient.h>

Expand All @@ -15,8 +16,12 @@ class MqttBattery : public BatteryProvider {
private:
bool _verboseLogging = false;
String _socTopic;
String _voltageTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();

void onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
std::optional<float> getFloat(std::string const& src, char const* topic);
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};
1 change: 1 addition & 0 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class PowerLimiterClass {
void announceStatus(Status status);
bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); }
float getBatteryVoltage(bool log = false);
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
bool canUseDirectSolarPower();
Expand Down
3 changes: 3 additions & 0 deletions include/VictronMppt.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class VictronMpptClass {
// sum of today's yield of all MPPT charge controllers in kWh
double getYieldDay() const;

// minimum of all MPPT charge controllers' output voltages in V
double getOutputVoltage() const;

private:
void loop();
VictronMpptClass(VictronMpptClass const& other) = delete;
Expand Down
1 change: 1 addition & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_IGNORE_SOC false
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
Expand Down
38 changes: 21 additions & 17 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const
root[F("manufacturer")] = _manufacturer;
root[F("data_age")] = getAgeSeconds();

addLiveViewValue(root, "SoC", _SoC, "%", 0);
addLiveViewValue(root, "SoC", _soc, "%", _socPrecision);
addLiveViewValue(root, "voltage", _voltage, "V", 2);
}

void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
Expand All @@ -68,7 +69,6 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1);
addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0);
addLiveViewValue(root, "voltage", _voltage, "V", 2);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "temperature", _temperature, "°C", 1);

Expand Down Expand Up @@ -105,18 +105,13 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const

using Label = JkBms::DataPointLabel;

auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value()) {
addLiveViewValue(root, "voltage",
static_cast<float>(*oVoltage) / 1000, "V", 2);
}

auto oCurrent = _dataPoints.get<Label::BatteryCurrentMilliAmps>();
if (oCurrent.has_value()) {
addLiveViewValue(root, "current",
static_cast<float>(*oCurrent) / 1000, "A", 2);
}

auto oVoltage = _dataPoints.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
auto voltage = static_cast<float>(*oVoltage) / 1000;
Expand Down Expand Up @@ -217,7 +212,8 @@ void BatteryStats::mqttPublish() const
{
MqttSettings.publish(F("battery/manufacturer"), _manufacturer);
MqttSettings.publish(F("battery/dataAge"), String(getAgeSeconds()));
MqttSettings.publish(F("battery/stateOfCharge"), String(_SoC));
MqttSettings.publish(F("battery/stateOfCharge"), String(_soc));
MqttSettings.publish(F("battery/voltage"), String(_voltage));
}

void PylontechBatteryStats::mqttPublish() const
Expand All @@ -228,7 +224,6 @@ void PylontechBatteryStats::mqttPublish() const
MqttSettings.publish(F("battery/settings/chargeCurrentLimitation"), String(_chargeCurrentLimitation));
MqttSettings.publish(F("battery/settings/dischargeCurrentLimitation"), String(_dischargeCurrentLimitation));
MqttSettings.publish(F("battery/stateOfHealth"), String(_stateOfHealth));
MqttSettings.publish(F("battery/voltage"), String(_voltage));
MqttSettings.publish(F("battery/current"), String(_current));
MqttSettings.publish(F("battery/temperature"), String(_temperature));
MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge));
Expand Down Expand Up @@ -260,6 +255,10 @@ void JkBmsBatteryStats::mqttPublish() const
Label::CellsMilliVolt, // complex data format
Label::ModificationPassword, // sensitive data
Label::BatterySoCPercent // already published by base class
// NOTE that voltage is also published by the base class, however, we
// previously published it only from here using the respective topic.
// to avoid a breaking change, we publish the value again using the
// "old" topic.
};

// regularly publish all topics regardless of whether or not their value changed
Expand Down Expand Up @@ -335,9 +334,16 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)

auto oSoCValue = dp.get<Label::BatterySoCPercent>();
if (oSoCValue.has_value()) {
_SoC = *oSoCValue;
auto oSoCDataPoint = dp.getDataPointFor<Label::BatterySoCPercent>();
_lastUpdateSoC = oSoCDataPoint->getTimestamp();
BatteryStats::setSoC(*oSoCValue, 0/*precision*/,
oSoCDataPoint->getTimestamp());
}

auto oVoltage = dp.get<Label::BatteryVoltageMilliVolt>();
if (oVoltage.has_value()) {
auto oVoltageDataPoint = dp.getDataPointFor<Label::BatteryVoltageMilliVolt>();
BatteryStats::setVoltage(static_cast<float>(*oVoltage) / 1000,
oVoltageDataPoint->getTimestamp());
}

_dataPoints.updateFrom(dp);
Expand All @@ -360,8 +366,9 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
}

void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) {
_SoC = shuntData.SOC / 10;
_voltage = shuntData.V;
BatteryStats::setVoltage(shuntData.V, millis());
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());

_current = shuntData.I;
_modelName = shuntData.getPidAsString().data();
_chargeCycles = shuntData.H4;
Expand All @@ -380,14 +387,12 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c
_alarmHighTemperature = shuntData.AR & 64;

_lastUpdate = VeDirectShunt.getLastUpdate();
_lastUpdateSoC = VeDirectShunt.getLastUpdate();
}

void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
BatteryStats::getLiveViewData(root);

// values go into the "Status" card of the web application
addLiveViewValue(root, "voltage", _voltage, "V", 2);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1);
Expand All @@ -406,7 +411,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
void VictronSmartShuntStats::mqttPublish() const {
BatteryStats::mqttPublish();

MqttSettings.publish(F("battery/voltage"), String(_voltage));
MqttSettings.publish(F("battery/current"), String(_current));
MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles));
MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy));
Expand Down
8 changes: 6 additions & 2 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ bool ConfigurationClass::write()
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold;
Expand All @@ -207,7 +208,8 @@ bool ConfigurationClass::write()
battery["provider"] = config.Battery.Provider;
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttTopic;
battery["mqtt_topic"] = config.Battery.MqttSocTopic;
battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;

JsonObject huawei = doc.createNestedObject("huawei");
huawei["enabled"] = config.Huawei.Enabled;
Expand Down Expand Up @@ -435,6 +437,7 @@ bool ConfigurationClass::read()
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
Expand All @@ -451,7 +454,8 @@ bool ConfigurationClass::read()
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
strlcpy(config.Battery.MqttTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttTopic));
strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic));
strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));

JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
Expand Down
Loading
Loading