diff --git a/include/BatteryStats.h b/include/BatteryStats.h index c527b827..94da35d7 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -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 { } }; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 34542b66..c4b428d3 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -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(); - if (oCurrent.has_value()) { - addLiveViewValue(root, "current", - static_cast(*oCurrent) / 1000, "A", 2); - } - auto oVoltage = _dataPoints.get(); if (oVoltage.has_value() && oCurrent.has_value()) { auto current = static_cast(*oCurrent) / 1000; @@ -304,8 +298,15 @@ void BatteryStats::mqttPublish() const { MqttSettings.publish("battery/manufacturer", _manufacturer); MqttSettings.publish("battery/dataAge", String(getAgeSeconds())); - MqttSettings.publish("battery/stateOfCharge", String(_soc)); - MqttSettings.publish("battery/voltage", String(_voltage)); + 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(); + if (oCurrent.has_value()) { + auto oCurrentDataPoint = dp.getDataPointFor(); + BatteryStats::setCurrent(static_cast(*oCurrent) / 1000, 2/*precision*/, + oCurrentDataPoint->getTimestamp()); + } + _dataPoints.updateFrom(dp); auto oCellVoltages = _dataPoints.get(); @@ -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(shuntData.SOC) / 10, 1/*precision*/, millis()); + BatteryStats::setCurrent(static_cast(shuntData.batteryCurrent_I_mA) / 1000, 2/*precision*/, millis()); _fwversion = shuntData.getFwVersionFormatted(); - _current = static_cast(shuntData.batteryCurrent_I_mA) / 1000; _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; _chargedEnergy = static_cast(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)); diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index f2ed9a51..517a6a23 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -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; diff --git a/src/PytesCanReceiver.cpp b/src/PytesCanReceiver.cpp index 805d59fa..81c7c85c 100644 --- a/src/PytesCanReceiver.cpp +++ b/src/PytesCanReceiver.cpp @@ -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; } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 17e5a52e..cda20e72 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -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(); } diff --git a/webapp/src/components/CardElement.vue b/webapp/src/components/CardElement.vue index 20bce75f..72c00f42 100644 --- a/webapp/src/components/CardElement.vue +++ b/webapp/src/components/CardElement.vue @@ -1,7 +1,7 @@