Skip to content

Commit

Permalink
Feature: JK BMS: export (more) data to live view and MQTT (#549)
Browse files Browse the repository at this point in the history
* add more values to web app live view. this should add all interesting
  values for the web app live view. those include important values and
  values that change frequently.

* add more interesting JK BMS dummy messages: one has 0% SoC and an
  alarm (discharge undervoltage) set. the other has the undertemperature
  alarm set.

* add alarms and warnings to live view

* publish alarm and status bits through MQTT individually

* publish cell voltages to MQTT broker

* remove trailing spaces in BatteryStats class
  • Loading branch information
schlimmchen authored Dec 15, 2023
1 parent dd8446d commit 6e78c5b
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 14 deletions.
20 changes: 17 additions & 3 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,29 @@ class PylontechBatteryStats : public BatteryStats {

class JkBmsBatteryStats : public BatteryStats {
public:
void getLiveViewData(JsonVariant& root) const final;
void getLiveViewData(JsonVariant& root) const final {
getJsonData(root, false);
}

void getInfoViewData(JsonVariant& root) const {
getJsonData(root, true);
}

void mqttPublish() const final;

void updateFrom(JkBms::DataPointContainer const& dp);

private:
void getJsonData(JsonVariant& root, bool verbose) const;

JkBms::DataPointContainer _dataPoints;
mutable uint32_t _lastMqttPublish = 0;
mutable uint32_t _lastFullMqttPublish = 0;

uint16_t _cellMinMilliVolt = 0;
uint16_t _cellAvgMilliVolt = 0;
uint16_t _cellMaxMilliVolt = 0;
uint32_t _cellVoltageTimestamp = 0;
};

class VictronSmartShuntStats : public BatteryStats {
Expand All @@ -107,8 +121,8 @@ class VictronSmartShuntStats : public BatteryStats {

void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData);

private:
float _voltage;
private:
float _voltage;
float _current;
float _temperature;
bool _tempPresent;
Expand Down
48 changes: 48 additions & 0 deletions include/JkBmsDataPoints.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,54 @@

namespace JkBms {

#define ALARM_BITS(fnc) \
fnc(LowCapacity, (1<<0)) \
fnc(BmsOvertemperature, (1<<1)) \
fnc(ChargingOvervoltage, (1<<2)) \
fnc(DischargeUndervoltage, (1<<3)) \
fnc(BatteryOvertemperature, (1<<4)) \
fnc(ChargingOvercurrent, (1<<5)) \
fnc(DischargeOvercurrent, (1<<6)) \
fnc(CellVoltageDifference, (1<<7)) \
fnc(BatteryBoxOvertemperature, (1<<8)) \
fnc(BatteryUndertemperature, (1<<9)) \
fnc(CellOvervoltage, (1<<10)) \
fnc(CellUndervoltage, (1<<11)) \
fnc(AProtect, (1<<12)) \
fnc(BProtect, (1<<13)) \
fnc(Reserved1, (1<<14)) \
fnc(Reserved2, (1<<15))

enum class AlarmBits : uint16_t {
#define ALARM_ENUM(name, value) name = value,
ALARM_BITS(ALARM_ENUM)
#undef ALARM_ENUM
};

static const std::map<AlarmBits, std::string> AlarmBitTexts = {
#define ALARM_TEXT(name, value) { AlarmBits::name, #name },
ALARM_BITS(ALARM_TEXT)
#undef ALARM_TEXT
};

#define STATUS_BITS(fnc) \
fnc(ChargingActive, (1<<0)) \
fnc(DischargingActive, (1<<1)) \
fnc(BalancingActive, (1<<2)) \
fnc(BatteryOnline, (1<<3))

enum class StatusBits : uint16_t {
#define STATUS_ENUM(name, value) name = value,
STATUS_BITS(STATUS_ENUM)
#undef STATUS_ENUM
};

static const std::map<StatusBits, std::string> StatusBitTexts = {
#define STATUS_TEXT(name, value) { StatusBits::name, #name },
STATUS_BITS(STATUS_TEXT)
#undef STATUS_TEXT
};

enum class DataPointLabel : uint8_t {
CellsMilliVolt = 0x79,
BmsTempCelsius = 0x80,
Expand Down
138 changes: 130 additions & 8 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const
addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal);
}

void JkBmsBatteryStats::getLiveViewData(JsonVariant& root) const
void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const
{
BatteryStats::getLiveViewData(root);

Expand All @@ -102,9 +102,80 @@ void JkBmsBatteryStats::getLiveViewData(JsonVariant& root) const
static_cast<float>(*oCurrent) / 1000, "A", 2);
}

auto oTemperature = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperature.has_value()) {
addLiveViewValue(root, "temperature", *oTemperature, "°C", 0);
if (oVoltage.has_value() && oCurrent.has_value()) {
auto current = static_cast<float>(*oCurrent) / 1000;
auto voltage = static_cast<float>(*oVoltage) / 1000;
addLiveViewValue(root, "power", current * voltage , "W", 2);
}

if (verbose) {
auto oTemperatureOne = _dataPoints.get<Label::BatteryTempOneCelsius>();
if (oTemperatureOne.has_value()) {
addLiveViewValue(root, "batOneTemp", *oTemperatureOne, "°C", 0);
}
}

if (verbose) {
auto oTemperatureTwo = _dataPoints.get<Label::BatteryTempTwoCelsius>();
if (oTemperatureTwo.has_value()) {
addLiveViewValue(root, "batTwoTemp", *oTemperatureTwo, "°C", 0);
}
}

auto oTemperatureBms = _dataPoints.get<Label::BmsTempCelsius>();
if (oTemperatureBms.has_value()) {
addLiveViewValue(root, "bmsTemp", *oTemperatureBms, "°C", 0);
}

if (_cellVoltageTimestamp > 0) {
if (verbose) {
addLiveViewValue(root, "cellMinVoltage", static_cast<float>(_cellMinMilliVolt)/1000, "V", 3);
}

addLiveViewValue(root, "cellAvgVoltage", static_cast<float>(_cellAvgMilliVolt)/1000, "V", 3);

if (verbose) {
addLiveViewValue(root, "cellMaxVoltage", static_cast<float>(_cellMaxMilliVolt)/1000, "V", 3);
addLiveViewValue(root, "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 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<Label::StatusBitmask>();
if (oStatus.has_value()) {
using Bits = JkBms::StatusBits;
auto chargeEnabled = *oStatus & static_cast<uint16_t>(Bits::ChargingActive);
addLiveViewText(root, "chargeEnabled", (chargeEnabled?"yes":"no"));
auto dischargeEnabled = *oStatus & static_cast<uint16_t>(Bits::DischargingActive);
addLiveViewText(root, "dischargeEnabled", (dischargeEnabled?"yes":"no"));
auto balancingActive = *oStatus & static_cast<uint16_t>(Bits::BalancingActive);
addLiveViewText(root, "balancingActive", (balancingActive?"yes":"no"));
}

auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
#define ISSUE(t, x) \
auto x = *oAlarms & static_cast<uint16_t>(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
}
}

Expand Down Expand Up @@ -174,6 +245,43 @@ void JkBmsBatteryStats::mqttPublish() const
MqttSettings.publish(topic, iter->second.getValueText().c_str());
}

auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value() && (fullPublish || _cellVoltageTimestamp > _lastMqttPublish)) {
unsigned idx = 1;
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
String topic("battery/Cell");
topic += String(idx);
topic += "MilliVolt";

MqttSettings.publish(topic, String(iter->second));

++idx;
}

MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt));
MqttSettings.publish("battery/CellAvgMilliVolt", String(_cellAvgMilliVolt));
MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt));
MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt));
}

auto oAlarms = _dataPoints.get<Label::AlarmsBitmask>();
if (oAlarms.has_value()) {
for (auto iter = JkBms::AlarmBitTexts.begin(); iter != JkBms::AlarmBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oAlarms & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/alarms/") + iter->second.c_str(), value);
}
}

auto oStatus = _dataPoints.get<Label::StatusBitmask>();
if (oStatus.has_value()) {
for (auto iter = JkBms::StatusBitTexts.begin(); iter != JkBms::StatusBitTexts.end(); ++iter) {
auto bit = iter->first;
String value = (*oStatus & static_cast<uint16_t>(bit))?"1":"0";
MqttSettings.publish(String("battery/status/") + iter->second.c_str(), value);
}
}

_lastMqttPublish = millis();
if (fullPublish) { _lastFullMqttPublish = _lastMqttPublish; }
}
Expand Down Expand Up @@ -201,6 +309,20 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)

_dataPoints.updateFrom(dp);

auto oCellVoltages = _dataPoints.get<Label::CellsMilliVolt>();
if (oCellVoltages.has_value()) {
for (auto iter = oCellVoltages->cbegin(); iter != oCellVoltages->cend(); ++iter) {
if (iter == oCellVoltages->cbegin()) {
_cellMinMilliVolt = _cellAvgMilliVolt = _cellMaxMilliVolt = iter->second;
continue;
}
_cellMinMilliVolt = std::min(_cellMinMilliVolt, iter->second);
_cellAvgMilliVolt = (_cellAvgMilliVolt + iter->second) / 2;
_cellMaxMilliVolt = std::max(_cellMaxMilliVolt, iter->second);
}
_cellVoltageTimestamp = millis();
}

_lastUpdate = millis();
}

Expand All @@ -216,7 +338,7 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c
_manufacturer = "Victron " + _modelName;
_temperature = shuntData.T;
_tempPresent = shuntData.tempPresent;

// shuntData.AR is a bitfield, so we need to check each bit individually
_alarmLowVoltage = shuntData.AR & 1;
_alarmHighVoltage = shuntData.AR & 2;
Expand All @@ -233,19 +355,19 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {

// values go into the "Status" card of the web application
addLiveViewValue(root, "voltage", _voltage, "V", 2);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "current", _current, "A", 1);
addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0);
addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1);
addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1);
if (_tempPresent) {
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
}

addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage);
addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage);
addLiveViewAlarm(root, "lowSOC", _alarmLowSOC);
addLiveViewAlarm(root, "lowTemperature", _alarmLowTemperature);
addLiveViewAlarm(root, "highTemperature", _alarmHighTemperature);
addLiveViewAlarm(root, "highTemperature", _alarmHighTemperature);
}

void VictronSmartShuntStats::mqttPublish() const {
Expand Down
78 changes: 78 additions & 0 deletions src/JkBmsController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,84 @@ class DummySerial {
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x4f, 0xc1
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13,
0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c,
0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07,
0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb,
0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b,
0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f,
0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18,
0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13,
0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02,
0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14,
0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00,
0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90,
0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05,
0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00,
0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x45, 0xce
},
{
0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00,
0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07,
0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c,
0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07,
0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08,
0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c,
0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f,
0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06,
0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13,
0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02,
0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a,
0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00,
0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90,
0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05,
0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00,
0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98,
0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e,
0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01,
0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00,
0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3,
0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03,
0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff,
0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6,
0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae,
0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14,
0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00,
0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65,
0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32,
0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d,
0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53,
0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f,
0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba,
0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42,
0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50,
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00,
0x00, 0x41, 0x7b
}
};
size_t _msg_idx = 0;
Expand Down
Loading

0 comments on commit 6e78c5b

Please sign in to comment.