// SPDX-License-Identifier: GPL-2.0-or-later #include #include #include "BatteryStats.h" #include "Configuration.h" #include "MqttSettings.h" #include "JkBmsDataPoints.h" #include "JbdBmsDataPoints.h" #include "MqttSettings.h" template static void addLiveViewInSection(JsonVariant& root, std::string const& section, std::string const& name, T&& value, std::string const& unit, uint8_t precision) { auto jsonValue = root["values"][section][name]; jsonValue["v"] = value; jsonValue["u"] = unit; jsonValue["d"] = precision; } template static void addLiveViewValue(JsonVariant& root, std::string const& name, T&& value, std::string const& unit, uint8_t precision) { addLiveViewInSection(root, "status", name, value, unit, precision); } static void addLiveViewTextInSection(JsonVariant& root, std::string const& section, std::string const& name, std::string const& text, bool translate = true) { auto jsonValue = root["values"][section][name]; jsonValue["value"] = text; jsonValue["translate"] = translate; } static void addLiveViewTextValue(JsonVariant& root, std::string const& name, std::string const& text) { addLiveViewTextInSection(root, "status", name, text); } static void addLiveViewWarning(JsonVariant& root, std::string const& name, bool warning) { if (!warning) { return; } root["issues"][name] = 1; } static void addLiveViewAlarm(JsonVariant& root, std::string const& name, bool alarm) { if (!alarm) { return; } root["issues"][name] = 2; } void BatteryStats::setManufacturer(const String& m) { String sanitized(m); for (int i = 0; i < sanitized.length(); i++) { char c = sanitized[i]; if (c < 0x20 || c >= 0x80) { sanitized.remove(i); // Truncate string break; } } _manufacturer = std::move(sanitized); } bool BatteryStats::updateAvailable(uint32_t since) const { if (_lastUpdate == 0) { return false; } // no data at all processed yet auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; return (_lastUpdate - since) < halfOfAllMillis; } void BatteryStats::getLiveViewData(JsonVariant& root) const { root["manufacturer"] = _manufacturer; if (!_serial.isEmpty()) { root["serial"] = _serial; } if (!_fwversion.isEmpty()) { root["fwversion"] = _fwversion; } if (!_hwversion.isEmpty()) { root["hwversion"] = _hwversion; } root["data_age"] = getAgeSeconds(); if (isSoCValid()) { addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); } if (isVoltageValid()) { addLiveViewValue(root, "voltage", _voltage, "V", 2); } if (isCurrentValid()) { addLiveViewValue(root, "current", _current, "A", _currentPrecision); } if (isDischargeCurrentLimitValid()) { addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1); } root["showIssues"] = supportsAlarmsAndWarnings(); } void MqttBatteryStats::getLiveViewData(JsonVariant& root) const { // as we don't want to repeat the data that is already shown in the live data card // we only add the live view data here when the discharge current limit can be shown if (isDischargeCurrentLimitValid()) { BatteryStats::getLiveViewData(root); } } void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); // values go into the "Status" card of the web application addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimitation, "V", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); addLiveViewValue(root, "temperature", _temperature, "°C", 1); addLiveViewValue(root, "modules", _moduleCount, "", 0); addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no")); addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no")); addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no")); // alarms and warnings go into the "Issues" card of the web application addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge); addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge); addLiveViewWarning(root, "highCurrentCharge", _warningHighCurrentCharge); addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge); addLiveViewWarning(root, "lowTemperature", _warningLowTemperature); addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); addLiveViewWarning(root, "highTemperature", _warningHighTemperature); addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); addLiveViewWarning(root, "lowVoltage", _warningLowVoltage); addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); addLiveViewWarning(root, "highVoltage", _warningHighVoltage); addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); addLiveViewWarning(root, "bmsInternal", _warningBmsInternal); addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal); } void SBSBatteryStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); // values go into the "Status" card of the web application addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "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")); addLiveViewTextValue(root, "dischargeEnabled", (_dischargeEnabled?"yes":"no")); // alarms and warnings go into the "Issues" card of the web application addLiveViewWarning(root, "highCurrentDischarge", _warningHighCurrentDischarge); addLiveViewWarning(root, "highCurrentCharge", _warningHighCurrentCharge); addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal); addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); } void PytesBatteryStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); // values go into the "Status" card of the web application addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1); addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); if (_chargeCycles != -1) { addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); } addLiveViewValue(root, "temperature", _temperature, "°C", 1); addLiveViewValue(root, "capacity", _totalCapacity, "Ah", _capacityPrecision); addLiveViewValue(root, "availableCapacity", _availableCapacity, "Ah", _capacityPrecision); if (_chargedEnergy != -1) { addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 1); } if (_dischargedEnergy != -1) { addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 1); } addLiveViewTextValue(root, "chargeImmediately", (_chargeImmediately?"yes":"no")); if (_balance != -1) { addLiveViewTextValue(root, "balancingActive", (_balance?"yes":"no")); } addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast(_cellMinMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast(_cellMaxMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0); addLiveViewInSection(root, "cells", "cellMinTemperature", _cellMinTemperature, "°C", 0); addLiveViewInSection(root, "cells", "cellMaxTemperature", _cellMaxTemperature, "°C", 0); addLiveViewTextInSection(root, "cells", "cellMinVoltageName", _cellMinVoltageName.c_str(), false); addLiveViewTextInSection(root, "cells", "cellMaxVoltageName", _cellMaxVoltageName.c_str(), false); addLiveViewTextInSection(root, "cells", "cellMinTemperatureName", _cellMinTemperatureName.c_str(), false); addLiveViewTextInSection(root, "cells", "cellMaxTemperatureName", _cellMaxTemperatureName.c_str(), false); addLiveViewInSection(root, "modules", "online", _moduleCountOnline, "", 0); addLiveViewInSection(root, "modules", "offline", _moduleCountOffline, "", 0); addLiveViewInSection(root, "modules", "blockingCharge", _moduleCountBlockingCharge, "", 0); addLiveViewInSection(root, "modules", "blockingDischarge", _moduleCountBlockingDischarge, "", 0); // alarms and warnings go into the "Issues" card of the web application addLiveViewWarning(root, "highCurrentDischarge", _warningHighDischargeCurrent); addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge); addLiveViewWarning(root, "highCurrentCharge", _warningHighChargeCurrent); addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge); addLiveViewWarning(root, "lowVoltage", _warningLowVoltage); addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); addLiveViewWarning(root, "highVoltage", _warningHighVoltage); addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); addLiveViewWarning(root, "lowTemperature", _warningLowTemperature); addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); addLiveViewWarning(root, "highTemperature", _warningHighTemperature); addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); addLiveViewWarning(root, "lowTemperatureCharge", _warningLowTemperatureCharge); addLiveViewAlarm(root, "underTemperatureCharge", _alarmUnderTemperatureCharge); addLiveViewWarning(root, "highTemperatureCharge", _warningHighTemperatureCharge); addLiveViewAlarm(root, "overTemperatureCharge", _alarmOverTemperatureCharge); addLiveViewWarning(root, "bmsInternal", _warningInternalFailure); addLiveViewAlarm(root, "bmsInternal", _alarmInternalFailure); addLiveViewWarning(root, "cellDiffVoltage", _warningCellImbalance); addLiveViewAlarm(root, "cellDiffVoltage", _alarmCellImbalance); } void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const { BatteryStats::getLiveViewData(root); using Label = JkBms::DataPointLabel; auto oCurrent = _dataPoints.get(); auto oVoltage = _dataPoints.get(); if (oVoltage.has_value() && oCurrent.has_value()) { auto current = static_cast(*oCurrent) / 1000; auto voltage = static_cast(*oVoltage) / 1000; addLiveViewValue(root, "power", current * voltage , "W", 2); } auto oTemperatureBms = _dataPoints.get(); if (oTemperatureBms.has_value()) { addLiveViewValue(root, "bmsTemp", *oTemperatureBms, "°C", 0); } // labels BatteryChargeEnabled, BatteryDischargeEnabled, and // BalancingEnabled refer to the user setting. we want to show the // actual MOSFETs' state which control whether charging and discharging // is possible and whether the BMS is currently balancing cells. auto oStatus = _dataPoints.get(); if (oStatus.has_value()) { using Bits = JkBms::StatusBits; auto chargeEnabled = *oStatus & static_cast(Bits::ChargingActive); addLiveViewTextValue(root, "chargeEnabled", (chargeEnabled?"yes":"no")); auto dischargeEnabled = *oStatus & static_cast(Bits::DischargingActive); addLiveViewTextValue(root, "dischargeEnabled", (dischargeEnabled?"yes":"no")); } auto oTemperatureOne = _dataPoints.get(); if (oTemperatureOne.has_value()) { addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0); } auto oTemperatureTwo = _dataPoints.get(); if (oTemperatureTwo.has_value()) { addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0); } if (_cellVoltageTimestamp > 0) { addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast(_cellMinMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast(_cellAvgMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast(_cellMaxMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0); } if (oStatus.has_value()) { using Bits = JkBms::StatusBits; auto balancingActive = *oStatus & static_cast(Bits::BalancingActive); addLiveViewTextInSection(root, "cells", "balancingActive", (balancingActive?"yes":"no")); } auto oAlarms = _dataPoints.get(); if (oAlarms.has_value()) { #define ISSUE(t, x) \ auto x = *oAlarms & static_cast(JkBms::AlarmBits::x); \ addLiveView##t(root, "JkBmsIssue"#x, x > 0); ISSUE(Warning, LowCapacity); ISSUE(Alarm, BmsOvertemperature); ISSUE(Alarm, ChargingOvervoltage); ISSUE(Alarm, DischargeUndervoltage); ISSUE(Alarm, BatteryOvertemperature); ISSUE(Alarm, ChargingOvercurrent); ISSUE(Alarm, DischargeOvercurrent); ISSUE(Alarm, CellVoltageDifference); ISSUE(Alarm, BatteryBoxOvertemperature); ISSUE(Alarm, BatteryUndertemperature); ISSUE(Alarm, CellOvervoltage); ISSUE(Alarm, CellUndervoltage); ISSUE(Alarm, AProtect); ISSUE(Alarm, BProtect); #undef ISSUE } } void JbdBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const { BatteryStats::getLiveViewData(root); using Label = JbdBms::DataPointLabel; auto oCurrent = _dataPoints.get(); auto oVoltage = _dataPoints.get(); if (oVoltage.has_value() && oCurrent.has_value()) { auto current = static_cast(*oCurrent) / 1000; auto voltage = static_cast(*oVoltage) / 1000; addLiveViewValue(root, "power", current * voltage , "W", 2); } auto oBatteryChargeEnabled = _dataPoints.get(); if (oBatteryChargeEnabled.has_value()) { addLiveViewTextValue(root, "chargeEnabled", (*oBatteryChargeEnabled?"yes":"no")); } auto oBatteryDischargeEnabled = _dataPoints.get(); if (oBatteryDischargeEnabled.has_value()) { addLiveViewTextValue(root, "dischargeEnabled", (*oBatteryDischargeEnabled?"yes":"no")); } auto oTemperatureOne = _dataPoints.get(); if (oTemperatureOne.has_value()) { addLiveViewInSection(root, "cells", "batOneTemp", *oTemperatureOne, "°C", 0); } auto oTemperatureTwo = _dataPoints.get(); if (oTemperatureTwo.has_value()) { addLiveViewInSection(root, "cells", "batTwoTemp", *oTemperatureTwo, "°C", 0); } if (_cellVoltageTimestamp > 0) { addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast(_cellMinMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellAvgVoltage", static_cast(_cellAvgMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast(_cellMaxMilliVolt)/1000, "V", 3); addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0); } auto oBalancingEnabled = _dataPoints.get(); if (oBalancingEnabled.has_value()) { addLiveViewTextInSection(root, "cells", "balancingActive", (*oBalancingEnabled?"yes":"no")); } auto oAlarms = _dataPoints.get(); if (oAlarms.has_value()) { #define ISSUE(t, x) \ auto x = *oAlarms & static_cast(JbdBms::AlarmBits::x); \ addLiveView##t(root, "JbdBmsIssue"#x, x > 0); //ISSUE(Warning, LowCapacity); ISSUE(Alarm, CellOverVoltage); ISSUE(Alarm, CellUnderVoltage); ISSUE(Alarm, PackOverVoltage); ISSUE(Alarm, PackUnderVoltage); ISSUE(Alarm, ChargingOverTemperature); ISSUE(Alarm, ChargingLowTemperature); ISSUE(Alarm, DischargingOverTemperature); ISSUE(Alarm, DischargingLowTemperature); ISSUE(Alarm, ChargingOverCurrent); ISSUE(Alarm, DischargeOverCurrent); ISSUE(Alarm, ShortCircuit); ISSUE(Alarm, IcFrontEndError); ISSUE(Alarm, MosSotwareLock); ISSUE(Alarm, Reserved1); ISSUE(Alarm, Reserved2); ISSUE(Alarm, Reserved3); #undef ISSUE } } void BatteryStats::mqttLoop() { auto& config = Configuration.get(); if (!MqttSettings.getConnected() || (millis() - _lastMqttPublish) < (config.Mqtt.PublishInterval * 1000)) { return; } mqttPublish(); _lastMqttPublish = millis(); } uint32_t BatteryStats::getMqttFullPublishIntervalMs() const { auto& config = Configuration.get(); // this is the default interval, see mqttLoop(). mqttPublish() // implementations in derived classes may choose to publish some values // with a lower frequency and hence implement this method with a different // return value. return config.Mqtt.PublishInterval * 1000; } 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)); } if (isDischargeCurrentLimitValid()) { MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit)); } } void PylontechBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimitation)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); MqttSettings.publish("battery/temperature", String(_temperature)); MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal)); MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge)); MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge)); MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal)); MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled)); MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled)); MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); MqttSettings.publish("battery/modulesTotal", String(_moduleCount)); } void SBSBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); MqttSettings.publish("battery/current", String(_current)); MqttSettings.publish("battery/temperature", String(_temperature)); MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal)); MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge)); MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge)); MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled)); MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled)); } void PytesBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltageLimit)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimit)); MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); if (_chargeCycles != -1) { MqttSettings.publish("battery/chargeCycles", String(_chargeCycles)); } if (_balance != -1) { MqttSettings.publish("battery/balancingActive", String(_balance ? 1 : 0)); } MqttSettings.publish("battery/temperature", String(_temperature)); if (_chargedEnergy != -1) { MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); } if (_dischargedEnergy != -1) { MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); } MqttSettings.publish("battery/capacity", String(_totalCapacity)); MqttSettings.publish("battery/availableCapacity", String(_availableCapacity)); MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt)); MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt)); MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt)); MqttSettings.publish("battery/CellMinTemperature", String(_cellMinTemperature)); MqttSettings.publish("battery/CellMaxTemperature", String(_cellMaxTemperature)); MqttSettings.publish("battery/CellMinVoltageName", String(_cellMinVoltageName)); MqttSettings.publish("battery/CellMaxVoltageName", String(_cellMaxVoltageName)); MqttSettings.publish("battery/CellMinTemperatureName", String(_cellMinTemperatureName)); MqttSettings.publish("battery/CellMaxTemperatureName", String(_cellMaxTemperatureName)); MqttSettings.publish("battery/modulesOnline", String(_moduleCountOnline)); MqttSettings.publish("battery/modulesOffline", String(_moduleCountOffline)); MqttSettings.publish("battery/modulesBlockingCharge", String(_moduleCountBlockingCharge)); MqttSettings.publish("battery/modulesBlockingDischarge", String(_moduleCountBlockingDischarge)); MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); MqttSettings.publish("battery/alarm/underTemperatureCharge", String(_alarmUnderTemperatureCharge)); MqttSettings.publish("battery/alarm/overTemperatureCharge", String(_alarmOverTemperatureCharge)); MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmInternalFailure)); MqttSettings.publish("battery/alarm/cellImbalance", String(_alarmCellImbalance)); MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighDischargeCurrent)); MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighChargeCurrent)); MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); MqttSettings.publish("battery/warning/lowTemperatureCharge", String(_warningLowTemperatureCharge)); MqttSettings.publish("battery/warning/highTemperatureCharge", String(_warningHighTemperatureCharge)); MqttSettings.publish("battery/warning/bmsInternal", String(_warningInternalFailure)); MqttSettings.publish("battery/warning/cellImbalance", String(_warningCellImbalance)); MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); } void JkBmsBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); using Label = JkBms::DataPointLabel; static std::vector