feature: show battery voltage, current, and power in live view (#1131)
* show battery voltage, current, and power in live view header (the "totals") * show battery current and power in extra card * use soc and current precision in live view * BatteryStats: do not knowingly publish invalid data: not all battery providers know all values the base class manages. make sure to prevent publishing invalid values. Co-authored-by: Bernhard Kirchen <schlimmchen@posteo.net>
This commit is contained in:
parent
e95b70efeb
commit
accc70dea0
@ -14,33 +14,37 @@ class BatteryStats {
|
||||
public:
|
||||
String const& getManufacturer() const { return _manufacturer; }
|
||||
|
||||
// the last time *any* datum was updated
|
||||
// the last time *any* data was updated
|
||||
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
|
||||
bool updateAvailable(uint32_t since) const;
|
||||
|
||||
uint8_t getSoC() const { return _soc; }
|
||||
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
|
||||
uint8_t getSoCPrecision() const { return _socPrecision; }
|
||||
|
||||
float getVoltage() const { return _voltage; }
|
||||
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }
|
||||
|
||||
float getChargeCurrent() const { return _current; };
|
||||
uint8_t getChargeCurrentPrecision() const { return _currentPrecision; }
|
||||
|
||||
// convert stats to JSON for web application live view
|
||||
virtual void getLiveViewData(JsonVariant& root) const;
|
||||
|
||||
void mqttLoop();
|
||||
|
||||
// the interval at which all battery datums will be re-published, even
|
||||
// the interval at which all battery data will be re-published, even
|
||||
// if they did not change. used to calculate Home Assistent expiration.
|
||||
virtual uint32_t getMqttFullPublishIntervalMs() const;
|
||||
|
||||
bool isSoCValid() const { return _lastUpdateSoC > 0; }
|
||||
bool isVoltageValid() const { return _lastUpdateVoltage > 0; }
|
||||
bool isCurrentValid() const { return _lastUpdateCurrent > 0; }
|
||||
|
||||
// returns true if the battery reached a critically low voltage/SoC,
|
||||
// such that it is in need of charging to prevent degredation.
|
||||
virtual bool getImmediateChargingRequest() const { return false; };
|
||||
|
||||
virtual float getChargeCurrent() const { return 0; };
|
||||
virtual float getChargeCurrentLimitation() const { return FLT_MAX; };
|
||||
|
||||
protected:
|
||||
@ -57,6 +61,12 @@ class BatteryStats {
|
||||
_lastUpdateVoltage = _lastUpdate = timestamp;
|
||||
}
|
||||
|
||||
void setCurrent(float current, uint8_t precision, uint32_t timestamp) {
|
||||
_current = current;
|
||||
_currentPrecision = precision;
|
||||
_lastUpdateCurrent = _lastUpdate = timestamp;
|
||||
}
|
||||
|
||||
String _manufacturer = "unknown";
|
||||
String _hwversion = "";
|
||||
String _fwversion = "";
|
||||
@ -70,6 +80,12 @@ class BatteryStats {
|
||||
uint32_t _lastUpdateSoC = 0;
|
||||
float _voltage = 0; // total battery pack voltage
|
||||
uint32_t _lastUpdateVoltage = 0;
|
||||
|
||||
// total current into (positive) or from (negative)
|
||||
// the battery, i.e., the charging current
|
||||
float _current = 0;
|
||||
uint8_t _currentPrecision = 0; // decimal places
|
||||
uint32_t _lastUpdateCurrent = 0;
|
||||
};
|
||||
|
||||
class PylontechBatteryStats : public BatteryStats {
|
||||
@ -79,7 +95,6 @@ class PylontechBatteryStats : public BatteryStats {
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
bool getImmediateChargingRequest() const { return _chargeImmediately; } ;
|
||||
float getChargeCurrent() const { return _current; } ;
|
||||
float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ;
|
||||
|
||||
private:
|
||||
@ -90,9 +105,6 @@ class PylontechBatteryStats : public BatteryStats {
|
||||
float _chargeCurrentLimitation;
|
||||
float _dischargeCurrentLimitation;
|
||||
uint16_t _stateOfHealth;
|
||||
// total current into (positive) or from (negative)
|
||||
// the battery, i.e., the charging current
|
||||
float _current;
|
||||
float _temperature;
|
||||
|
||||
bool _alarmOverCurrentDischarge;
|
||||
@ -122,7 +134,6 @@ class PytesBatteryStats : public BatteryStats {
|
||||
public:
|
||||
void getLiveViewData(JsonVariant& root) const final;
|
||||
void mqttPublish() const final;
|
||||
float getChargeCurrent() const { return _current; } ;
|
||||
float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ;
|
||||
|
||||
private:
|
||||
@ -144,9 +155,6 @@ class PytesBatteryStats : public BatteryStats {
|
||||
|
||||
uint16_t _stateOfHealth;
|
||||
|
||||
// total current into (positive) or from (negative)
|
||||
// the battery, i.e., the charging current
|
||||
float _current;
|
||||
float _temperature;
|
||||
|
||||
uint16_t _cellMinMilliVolt;
|
||||
@ -231,7 +239,6 @@ class VictronSmartShuntStats : public BatteryStats {
|
||||
void updateFrom(VeDirectShuntController::data_t const& shuntData);
|
||||
|
||||
private:
|
||||
float _current;
|
||||
float _temperature;
|
||||
bool _tempPresent;
|
||||
uint8_t _chargeCycles;
|
||||
@ -259,7 +266,7 @@ class MqttBatteryStats : public BatteryStats {
|
||||
// we do NOT publish the same data under a different topic.
|
||||
void mqttPublish() const final { }
|
||||
|
||||
// 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
|
||||
// we don't need a card in the liveview, since the SoC and
|
||||
// voltage (if available) is already displayed at the top.
|
||||
void getLiveViewData(JsonVariant& root) const final { }
|
||||
};
|
||||
|
||||
@ -78,6 +78,7 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
|
||||
addLiveViewValue(root, "SoC", _soc, "%", _socPrecision);
|
||||
addLiveViewValue(root, "voltage", _voltage, "V", 2);
|
||||
addLiveViewValue(root, "current", _current, "A", _currentPrecision);
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
@ -89,7 +90,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, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "temperature", _temperature, "°C", 1);
|
||||
|
||||
addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no"));
|
||||
@ -124,7 +124,6 @@ void PytesBatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
// values go into the "Status" card of the web application
|
||||
addLiveViewValue(root, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1);
|
||||
addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1);
|
||||
addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1);
|
||||
@ -198,11 +197,6 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
|
||||
using Label = JkBms::DataPointLabel;
|
||||
|
||||
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;
|
||||
@ -304,9 +298,16 @@ void BatteryStats::mqttPublish() const
|
||||
{
|
||||
MqttSettings.publish("battery/manufacturer", _manufacturer);
|
||||
MqttSettings.publish("battery/dataAge", String(getAgeSeconds()));
|
||||
if (isSoCValid()) {
|
||||
MqttSettings.publish("battery/stateOfCharge", String(_soc));
|
||||
}
|
||||
if (isVoltageValid()) {
|
||||
MqttSettings.publish("battery/voltage", String(_voltage));
|
||||
}
|
||||
if (isCurrentValid()) {
|
||||
MqttSettings.publish("battery/current", String(_current));
|
||||
}
|
||||
}
|
||||
|
||||
void PylontechBatteryStats::mqttPublish() const
|
||||
{
|
||||
@ -316,7 +317,6 @@ void PylontechBatteryStats::mqttPublish() const
|
||||
MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation));
|
||||
MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation));
|
||||
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
|
||||
MqttSettings.publish("battery/current", String(_current));
|
||||
MqttSettings.publish("battery/temperature", String(_temperature));
|
||||
MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge));
|
||||
MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge));
|
||||
@ -347,7 +347,6 @@ void PytesBatteryStats::mqttPublish() const
|
||||
MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit));
|
||||
|
||||
MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth));
|
||||
MqttSettings.publish("battery/current", String(_current));
|
||||
MqttSettings.publish("battery/temperature", String(_temperature));
|
||||
|
||||
if (_chargedEnergy != -1) {
|
||||
@ -505,6 +504,13 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||
oVoltageDataPoint->getTimestamp());
|
||||
}
|
||||
|
||||
auto oCurrent = dp.get<Label::BatteryCurrentMilliAmps>();
|
||||
if (oCurrent.has_value()) {
|
||||
auto oCurrentDataPoint = dp.getDataPointFor<Label::BatteryCurrentMilliAmps>();
|
||||
BatteryStats::setCurrent(static_cast<float>(*oCurrent) / 1000, 2/*precision*/,
|
||||
oCurrentDataPoint->getTimestamp());
|
||||
}
|
||||
|
||||
_dataPoints.updateFrom(dp);
|
||||
|
||||
auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
|
||||
@ -545,9 +551,9 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
|
||||
void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) {
|
||||
BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis());
|
||||
BatteryStats::setSoC(static_cast<float>(shuntData.SOC) / 10, 1/*precision*/, millis());
|
||||
BatteryStats::setCurrent(static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000, 2/*precision*/, millis());
|
||||
_fwversion = shuntData.getFwVersionFormatted();
|
||||
|
||||
_current = static_cast<float>(shuntData.batteryCurrent_I_mA) / 1000;
|
||||
_chargeCycles = shuntData.H4;
|
||||
_timeToGo = shuntData.TTG / 60;
|
||||
_chargedEnergy = static_cast<float>(shuntData.H18) / 100;
|
||||
@ -574,7 +580,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
|
||||
BatteryStats::getLiveViewData(root);
|
||||
|
||||
// values go into the "Status" card of the web application
|
||||
addLiveViewValue(root, "current", _current, "A", 1);
|
||||
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
|
||||
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2);
|
||||
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2);
|
||||
@ -597,7 +602,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
|
||||
void VictronSmartShuntStats::mqttPublish() const {
|
||||
BatteryStats::mqttPublish();
|
||||
|
||||
MqttSettings.publish("battery/current", String(_current));
|
||||
MqttSettings.publish("battery/chargeCycles", String(_chargeCycles));
|
||||
MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy));
|
||||
MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy));
|
||||
|
||||
@ -39,12 +39,12 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message)
|
||||
|
||||
case 0x356: {
|
||||
_stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis());
|
||||
_stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis());
|
||||
_stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\r\n",
|
||||
_stats->getVoltage(), _stats->_current, _stats->_temperature);
|
||||
_stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -157,7 +157,7 @@ void PylontechCanReceiver::dummyData()
|
||||
_stats->_dischargeCurrentLimitation = dummyFloat(12);
|
||||
_stats->_stateOfHealth = 99;
|
||||
_stats->setVoltage(48.67, millis());
|
||||
_stats->_current = dummyFloat(-1);
|
||||
_stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis());
|
||||
_stats->_temperature = dummyFloat(20);
|
||||
|
||||
_stats->_chargeEnabled = true;
|
||||
|
||||
@ -40,12 +40,12 @@ void PytesCanReceiver::onMessage(twai_message_t rx_message)
|
||||
|
||||
case 0x356: {
|
||||
_stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis());
|
||||
_stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1);
|
||||
_stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis());
|
||||
_stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1);
|
||||
|
||||
if (_verboseLogging) {
|
||||
MessageOutput.printf("[Pytes] voltage: %f current: %f temperature: %f\r\n",
|
||||
_stats->getVoltage(), _stats->_current, _stats->_temperature);
|
||||
_stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@ -92,7 +92,21 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al
|
||||
batteryObj["enabled"] = config.Battery.Enabled;
|
||||
|
||||
if (config.Battery.Enabled) {
|
||||
addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0);
|
||||
if (spStats->isSoCValid()) {
|
||||
addTotalField(batteryObj, "soc", spStats->getSoC(), "%", spStats->getSoCPrecision());
|
||||
}
|
||||
|
||||
if (spStats->isVoltageValid()) {
|
||||
addTotalField(batteryObj, "voltage", spStats->getVoltage(), "V", 2);
|
||||
}
|
||||
|
||||
if (spStats->isCurrentValid()) {
|
||||
addTotalField(batteryObj, "current", spStats->getChargeCurrent(), "A", spStats->getChargeCurrentPrecision());
|
||||
}
|
||||
|
||||
if (spStats->isVoltageValid() && spStats->isCurrentValid()) {
|
||||
addTotalField(batteryObj, "power", spStats->getVoltage() * spStats->getChargeCurrent(), "W", 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!all) { _lastPublishBattery = millis(); }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="['card', addSpace ? 'mt-5' : '' ]">
|
||||
<div :class="['card-header', textVariant]">{{ text }}</div>
|
||||
<div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']">
|
||||
<div :class="['card-body', 'card-text', centerContent ? 'text-center' : '', flexChildren ? 'd-flex' : '']">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@ -16,6 +16,7 @@ export default defineComponent({
|
||||
'textVariant': String,
|
||||
'addSpace': Boolean,
|
||||
'centerContent': Boolean,
|
||||
'flexChildren': Boolean,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -66,8 +66,10 @@
|
||||
</h2>
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col" v-if="totalBattData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.BatterySoc')">
|
||||
<template v-if="totalBattData.enabled">
|
||||
<div class="col">
|
||||
<CardElement centerContent flexChildren textVariant="text-bg-success" :text="$t('invertertotalinfo.BatteryCharge')">
|
||||
<div class="flex-fill" v-if="totalBattData.soc">
|
||||
<h2>
|
||||
{{ $n(totalBattData.soc.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.soc.d,
|
||||
@ -75,8 +77,43 @@
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalBattData.soc.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" v-if="totalBattData.voltage">
|
||||
<h2>
|
||||
{{ $n(totalBattData.voltage.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.voltage.d,
|
||||
maximumFractionDigits: totalBattData.voltage.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalBattData.voltage.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</CardElement>
|
||||
</div>
|
||||
<div class="col" v-if="totalBattData.power || totalBattData.current">
|
||||
<CardElement centerContent flexChildren textVariant="text-bg-success" :text="$t('invertertotalinfo.BatteryPower')">
|
||||
<div class="flex-fill" v-if="totalBattData.power">
|
||||
<h2>
|
||||
{{ $n(totalBattData.power.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.power.d,
|
||||
maximumFractionDigits: totalBattData.power.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalBattData.power.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" v-if="totalBattData.current">
|
||||
<h2>
|
||||
{{ $n(totalBattData.current.v, 'decimal', {
|
||||
minimumFractionDigits: totalBattData.current.d,
|
||||
maximumFractionDigits: totalBattData.current.d
|
||||
}) }}
|
||||
<small class="text-muted">{{ totalBattData.current.u }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</CardElement>
|
||||
</div>
|
||||
</template>
|
||||
<div class="col" v-if="powerMeterData.enabled">
|
||||
<CardElement centerContent textVariant="text-bg-success" :text="$t('invertertotalinfo.HomePower')">
|
||||
<h2>
|
||||
|
||||
@ -395,7 +395,8 @@
|
||||
"MpptTotalYieldTotal": "MPPT Gesamtertrag Insgesamt",
|
||||
"MpptTotalYieldDay": "MPPT Gesamtertrag Heute",
|
||||
"MpptTotalPower": "MPPT Gesamtleistung",
|
||||
"BatterySoc": "Ladezustand",
|
||||
"BatteryCharge": "Batterie Ladezustand",
|
||||
"BatteryPower": "Batterie Leistung",
|
||||
"HomePower": "Leistung / Netz",
|
||||
"HuaweiPower": "Huawei AC Leistung"
|
||||
},
|
||||
|
||||
@ -397,7 +397,8 @@
|
||||
"MpptTotalYieldTotal": "MPPT Total Yield Total",
|
||||
"MpptTotalYieldDay": "MPPT Total Yield Day",
|
||||
"MpptTotalPower": "MPPT Total Power",
|
||||
"BatterySoc": "State of charge",
|
||||
"BatteryCharge": "Battery Charge",
|
||||
"BatteryPower": "Battery Power",
|
||||
"HomePower": "Grid Power",
|
||||
"HuaweiPower": "Huawei AC Power"
|
||||
},
|
||||
|
||||
@ -430,7 +430,8 @@
|
||||
"MpptTotalYieldTotal": "MPPT rendement total",
|
||||
"MpptTotalYieldDay": "MPPT rendement du jour",
|
||||
"MpptTotalPower": "MPPT puissance de l'installation",
|
||||
"BatterySoc": "State of charge",
|
||||
"BatteryCharge": "Battery Charge",
|
||||
"BatteryPower": "Battery Power",
|
||||
"HomePower": "Grid Power",
|
||||
"HuaweiPower": "Huawei AC Power"
|
||||
},
|
||||
|
||||
@ -61,7 +61,10 @@ export interface Huawei {
|
||||
|
||||
export interface Battery {
|
||||
enabled: boolean;
|
||||
soc: ValueObject;
|
||||
soc?: ValueObject;
|
||||
voltage?: ValueObject;
|
||||
power?: ValueObject;
|
||||
current?: ValueObject;
|
||||
}
|
||||
|
||||
export interface PowerMeter {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user