// SPDX-License-Identifier: GPL-2.0-or-later #include #include #include "BatteryStats.h" #include "Configuration.h" #include "MqttSettings.h" #include "JkBmsDataPoints.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) { root["values"][section][name] = text; } 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; } 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 (!_fwversion.isEmpty()) { root["fwversion"] = _fwversion; } if (!_hwversion.isEmpty()) { root["hwversion"] = _hwversion; } root["data_age"] = getAgeSeconds(); addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); addLiveViewValue(root, "voltage", _voltage, "V", 2); } 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, "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")); 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 JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const { BatteryStats::getLiveViewData(root); 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; 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 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())); MqttSettings.publish("battery/stateOfCharge", String(_soc)); MqttSettings.publish("battery/voltage", String(_voltage)); } void PylontechBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); 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)); 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)); } void JkBmsBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); using Label = JkBms::DataPointLabel; static std::vector